@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.
- package/dist/billing/BillingAdminRoutes.js +110 -71
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/BillingRoot.js +81 -73
- package/dist/billing/BillingRoot.js.map +1 -1
- package/dist/billing/BillingUserRoutes.js +103 -90
- package/dist/billing/BillingUserRoutes.js.map +1 -1
- package/dist/billing/modules/billing/index.js +2 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +135 -126
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +417 -0
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -0
- package/dist/billing/modules/billing/pages/index.js +1 -0
- package/dist/billing/modules/checkout/hooks/useCheckout.js +163 -161
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js +344 -167
- package/dist/billing/modules/coupons/pages/CreateCouponPage.js.map +1 -1
- package/dist/billing/modules/plans/constants/featuredQuotas.js +37 -0
- package/dist/billing/modules/plans/constants/featuredQuotas.js.map +1 -0
- package/dist/billing/modules/plans/index.js +2 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +205 -0
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -0
- package/dist/billing/modules/plans/pages/PlanCreatePage.js +51 -33
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +78 -60
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +246 -126
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/index.js +1 -0
- package/dist/billing/modules/promotions/index.js +4 -0
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +289 -0
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -0
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js +168 -0
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -0
- package/dist/billing/modules/promotions/pages/index.js +2 -0
- package/dist/billing/shared/types/graphql.js.map +1 -1
- package/dist/generated/global-operations.js +422 -290
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js +3 -1
- package/dist/generated/global-types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PlansBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansBrowsePage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans Browse Page (User-facing)\n * Browse available plans and subscribe. No create/edit/toggle actions.\n */\n\nimport { useState, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { usePlans } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport type { Plan, PlanDuration, PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\n\ntype ViewMode = 'grid' | 'list';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\nexport const PlansBrowsePage: 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 { productId } = useBilling();\n const { plans, isLoading, error, refetch } = usePlans({\n includeFree: false,\n productId: productId ?? undefined,\n });\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration | 'all'>('all');\n\n // Only show active plans to users\n const filteredPlans = plans.filter((plan) => {\n if (!plan.isActive) return false;\n if (billingInterval !== 'all' && plan.duration !== billingInterval) return false;\n return true;\n });\n\n if (isLoading && plans.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-muted animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlans', 'Unable to load plans.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.plans.browsePlansTitle', 'Plans')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.plans.browsePlansSubtitle', 'Choose a plan that fits your needs')}\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n {(['all', 'monthly', 'yearly'] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval as PlanDuration | 'all')}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {interval === 'all' ? 'All' : interval === 'monthly' ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.plans.noPlansAvailable', 'No plans available')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.plans.checkBackLater', 'Check back later for new plans.')}\n </p>\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredPlans.map((plan) => (\n <BrowsePlanCard\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <BrowsePlanRow\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Browse Plan Card (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanCard: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const combinedQuotas = combineQuotas(plan.features || []);\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"border border-border rounded-lg bg-card p-6 transition-all hover:shadow-md flex flex-col h-full\">\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-foreground\">{plan.name}</h3>\n <span className=\"text-sm text-muted-foreground\">\n {formatPlanDurationLabel(plan.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-3xl font-bold text-foreground\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-muted-foreground text-sm\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n\n {combinedQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4\">\n {combinedQuotas.slice(0, 5).map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-foreground\">{quota.name}</span>\n </div>\n <span className=\"font-medium text-foreground\">\n {quota.totalValue.toLocaleString()}\n </span>\n </li>\n ))}\n {combinedQuotas.length > 5 && (\n <li className=\"text-sm text-muted-foreground pl-6\">\n +{combinedQuotas.length - 5} more\n </li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border mt-auto\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"w-full px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Plan Row (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanRow: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const combinedQuotas = combineQuotas(plan.features || []);\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"flex items-center gap-4 p-4 border border-border rounded-lg bg-card transition-all hover:shadow-sm\">\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"font-semibold text-foreground truncate\">{plan.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {combinedQuotas.length} quota{combinedQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-foreground\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-muted-foreground\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View\n </button>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PlansBrowsePage.js","names":[],"sources":["../../../../../src/billing/modules/plans/pages/PlansBrowsePage.tsx"],"sourcesContent":["/**\n * Plans Module - Plans Browse Page (User-facing)\n * Browse available plans and subscribe. No create/edit/toggle actions.\n */\n\nimport { useState, type FC } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { usePlans } from '../hooks';\nimport { ServerError } from '../../../shared/components';\nimport {\n formatCurrency,\n formatPlanDuration,\n formatPlanDurationLabel,\n getPlanFeatureQuantity,\n isServerError,\n} from '../../../shared/utils';\nimport { PlanDuration } from '../../../shared/types';\nimport type { Plan, PlanFeature } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n useDefaultBillingCurrency,\n getDisplayPrice,\n} from '../../../hooks/useDefaultBillingCurrency';\nimport {\n FEATURED_QUOTA_NAMES,\n FEATURED_QUOTA_LABELS,\n formatFeaturedQuotaValue,\n} from '../constants/featuredQuotas';\n\ntype ViewMode = 'grid' | 'list';\n\ninterface CombinedQuota {\n quotaId: string;\n name: string;\n totalValue: number;\n}\n\nfunction combineQuotas(features: PlanFeature[]): CombinedQuota[] {\n const quotaMap = new Map<string, CombinedQuota>();\n for (const feature of features) {\n if (!feature.quota) continue;\n const quotaId = feature.quota.id;\n const limitValue = getPlanFeatureQuantity(feature);\n const existing = quotaMap.get(quotaId);\n if (existing) {\n existing.totalValue += limitValue;\n } else {\n quotaMap.set(quotaId, {\n quotaId,\n name: feature.quota.name ?? 'Unknown Quota',\n totalValue: limitValue,\n });\n }\n }\n return Array.from(quotaMap.values());\n}\n\n/** Returns only the featured quotas, in the canonical display order. */\nfunction getFeaturedQuotas(features: PlanFeature[]): CombinedQuota[] {\n const all = combineQuotas(features);\n const byName = new Map(all.map((q) => [q.name, q]));\n return FEATURED_QUOTA_NAMES.flatMap((name) => {\n const q = byName.get(name);\n return q ? [q] : [];\n });\n}\n\nexport const PlansBrowsePage: 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 { productId } = useBilling();\n const { plans, isLoading, error, refetch } = usePlans({\n includeFree: false,\n productId: productId ?? undefined,\n });\n\n const [viewMode, setViewMode] = useState<ViewMode>('grid');\n const [billingInterval, setBillingInterval] = useState<PlanDuration>(PlanDuration.MONTHLY);\n\n // Only show active plans to users\n const filteredPlans = plans.filter((plan) => plan.isActive && plan.duration === billingInterval);\n\n if (isLoading && plans.length === 0) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-muted animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"border border-border rounded-lg p-6 space-y-4\">\n <div className=\"h-6 w-24 bg-muted animate-pulse rounded\" />\n <div className=\"h-8 w-32 bg-muted animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3].map((j) => (\n <div key={j} className=\"h-4 w-full bg-muted animate-pulse rounded\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title={tr('billing.plans.serverUnavailable', 'Server Unavailable')}\n message={tr('billing.plans.unableToLoadPlans', 'Unable to load plans.')}\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <div>\n <h1 className=\"text-2xl font-bold text-foreground\">\n {tr('billing.plans.browsePlansTitle', 'Plans')}\n </h1>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.plans.browsePlansSubtitle', 'Choose a plan that fits your needs')}\n </p>\n </div>\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border\">\n <div className=\"inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n {([PlanDuration.MONTHLY, PlanDuration.YEARLY] as const).map((interval) => (\n <button\n type=\"button\"\n key={interval}\n onClick={() => setBillingInterval(interval)}\n className={`px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${\n billingInterval === interval\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {interval === PlanDuration.MONTHLY ? 'Monthly' : 'Yearly'}\n </button>\n ))}\n </div>\n\n <div className=\"ml-auto inline-flex rounded-lg border border-border p-1 bg-muted/50\">\n <button\n type=\"button\"\n onClick={() => setViewMode('grid')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'grid'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n title=\"Grid view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z\"\n />\n </svg>\n </button>\n <button\n type=\"button\"\n onClick={() => setViewMode('list')}\n className={`p-1.5 rounded-md transition-colors ${\n viewMode === 'list'\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n title=\"List view\"\n >\n <svg className=\"size-4\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 6h16M4 12h16M4 18h16\"\n />\n </svg>\n </button>\n </div>\n </div>\n\n {/* Plans */}\n {filteredPlans.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center py-12 border border-dashed border-border rounded-lg\">\n <div className=\"size-12 rounded-full bg-muted flex items-center justify-center mb-4\">\n <svg\n className=\"size-6 text-muted-foreground\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-medium text-foreground\">\n {tr('billing.plans.noPlansAvailable', 'No plans available')}\n </h3>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {tr('billing.plans.checkBackLater', 'Check back later for new plans.')}\n </p>\n </div>\n ) : viewMode === 'grid' ? (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {filteredPlans.map((plan) => (\n <BrowsePlanCard\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseCard />\n </div>\n ) : (\n <div className=\"space-y-3\">\n {filteredPlans.map((plan) => (\n <BrowsePlanRow\n key={plan.id}\n plan={plan}\n onView={() => navigateTo(`/plans/${plan.id}`)}\n onSubscribe={() => navigateTo(`/checkout/${plan.id}`)}\n />\n ))}\n <EnterpriseRow />\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Static Enterprise \"Contact Us\" card — no backend plan record\n// ============================================================================\n\nconst ENTERPRISE_HIGHLIGHTS = [\n 'Unlimited Agents & Sessions',\n 'Dedicated single-tenant infra',\n 'Customer-managed encryption keys',\n 'SSO / SCIM provisioning',\n 'Custom SLA & support contract',\n 'Volume-based custom pricing',\n 'Audit log with custom retention',\n 'Priority onboarding & migration',\n];\n\nconst EnterpriseCard: FC = () => (\n <div className=\"border border-action-primary-bg/40 bg-action-primary-bg/5 rounded-lg p-6 flex flex-col h-full\">\n <div className=\"mb-4\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-lg font-bold text-foreground\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-action-primary-bg/15 text-action-primary-bg\">\n Contact Us\n </span>\n </div>\n <span className=\"text-sm text-muted-foreground\">Custom contract</span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-2xl font-bold text-foreground\">Custom pricing</span>\n </div>\n\n <ul className=\"space-y-2 mb-4 flex-1\">\n {ENTERPRISE_HIGHLIGHTS.map((item) => (\n <li key={item} className=\"flex items-center gap-2 text-sm\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M5 13l4 4L19 7\" />\n </svg>\n <span className=\"text-foreground\">{item}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"space-y-2 pt-4 border-t border-border mt-auto\">\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"block w-full px-4 py-2 text-sm font-medium text-center bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n Contact Us\n </a>\n </div>\n </div>\n);\n\nconst EnterpriseRow: FC = () => (\n <div className=\"flex items-center gap-4 p-4 border border-action-primary-bg/40 rounded-lg bg-action-primary-bg/5 transition-all hover:shadow-sm\">\n <div className=\"size-10 rounded-lg bg-action-primary-bg/10 flex items-center justify-center shrink-0\">\n <svg\n className=\"size-5 text-action-primary-bg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-2 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"font-semibold text-foreground\">Enterprise</h3>\n <span className=\"text-xs font-medium px-2 py-0.5 rounded-full bg-action-primary-bg/15 text-action-primary-bg shrink-0\">\n Contact Us\n </span>\n </div>\n <p className=\"text-sm text-muted-foreground\">Dedicated infra · SSO/SCIM · Custom SLA</p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-foreground\">Custom pricing</div>\n <div className=\"text-sm text-muted-foreground\">contract-based</div>\n </div>\n\n <a\n href=\"mailto:contact@burdenoff.com?subject=Enterprise Plan Inquiry\"\n className=\"px-3 py-1.5 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors shrink-0\"\n >\n Contact Us\n </a>\n </div>\n);\n\n// ============================================================================\n// Browse Plan Card (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanCard: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const allQuotas = combineQuotas(plan.features || []);\n const extraCount = allQuotas.length - featuredQuotas.length;\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"border border-border rounded-lg bg-card p-6 transition-all hover:shadow-md flex flex-col h-full\">\n <div className=\"mb-4\">\n <h3 className=\"text-lg font-bold text-foreground\">{plan.name}</h3>\n <span className=\"text-sm text-muted-foreground\">\n {formatPlanDurationLabel(plan.duration)}\n </span>\n </div>\n\n <div className=\"flex items-baseline gap-1 mb-4\">\n <span className=\"text-3xl font-bold text-foreground\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </span>\n <span className=\"text-muted-foreground text-sm\">/ {formatPlanDuration(plan.duration)}</span>\n </div>\n\n {featuredQuotas.length > 0 && (\n <ul className=\"space-y-2 mb-4 flex-1\">\n {featuredQuotas.map((quota) => (\n <li key={quota.quotaId} className=\"flex items-center justify-between text-sm\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-4 text-status-success-text shrink-0\"\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=\"M5 13l4 4L19 7\"\n />\n </svg>\n <span className=\"text-foreground\">\n {FEATURED_QUOTA_LABELS[quota.name] ?? quota.name}\n </span>\n </div>\n <span className=\"font-medium text-foreground tabular-nums\">\n {formatFeaturedQuotaValue(quota.name, quota.totalValue)}\n </span>\n </li>\n ))}\n {extraCount > 0 && (\n <li className=\"text-sm text-muted-foreground pl-6\">+{extraCount} more — see details</li>\n )}\n </ul>\n )}\n\n <div className=\"space-y-2 pt-4 border-t border-border mt-auto\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"w-full px-4 py-2 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"w-full px-4 py-2 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View Details\n </button>\n </div>\n </div>\n );\n};\n\n// ============================================================================\n// Browse Plan Row (no edit/toggle)\n// ============================================================================\n\nconst BrowsePlanRow: FC<{ plan: Plan; onView: () => void; onSubscribe: () => void }> = ({\n plan,\n onView,\n onSubscribe,\n}) => {\n const featuredQuotas = getFeaturedQuotas(plan.features || []);\n const displayCurrency = useDefaultBillingCurrency();\n\n return (\n <div className=\"flex items-center gap-4 p-4 border border-border rounded-lg bg-card transition-all hover:shadow-sm\">\n <div className=\"size-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0\">\n <svg className=\"size-5 text-primary\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <h3 className=\"font-semibold text-foreground truncate\">{plan.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {featuredQuotas.length} key feature{featuredQuotas.length !== 1 ? 's' : ''} included\n </p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"font-semibold text-foreground\">\n {(() => {\n const { price, currency } = getDisplayPrice(\n plan.price,\n plan.currency,\n plan.currencyPrices as Record<string, number> | null,\n displayCurrency\n );\n return formatCurrency(price, currency);\n })()}\n </div>\n <div className=\"text-sm text-muted-foreground\">per {formatPlanDuration(plan.duration)}</div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={onSubscribe}\n className=\"px-3 py-1.5 text-sm font-medium bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors\"\n >\n Subscribe\n </button>\n <button\n type=\"button\"\n onClick={onView}\n className=\"px-3 py-1.5 text-sm font-medium border border-border text-foreground rounded-md hover:bg-muted transition-colors\"\n >\n View\n </button>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { AccessDenied as e } from "../../../shared/components/AccessDenied.js";
|
|
2
|
+
import { PageHeader as t } from "../../../shared/components/PageHeader.js";
|
|
3
|
+
import "../../../shared/components/index.js";
|
|
4
|
+
import { useBillingPermissions as n } from "../../../hooks/useBillingPermissions.js";
|
|
5
|
+
import { useCreatePromotionMutation as r } from "../../../../generated/global-operations.js";
|
|
6
|
+
import { useBillingNavigate as i } from "../../../hooks/useBillingNavigate.js";
|
|
7
|
+
import { useState as a } from "react";
|
|
8
|
+
import { ArrowLeft as o, Gift as s, Info as c, Save as l } from "lucide-react";
|
|
9
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
10
|
+
//#region src/billing/modules/promotions/pages/CreatePromotionPage.tsx
|
|
11
|
+
var f = [
|
|
12
|
+
"vibecontrols",
|
|
13
|
+
"workspaces",
|
|
14
|
+
"botlit",
|
|
15
|
+
"fluidgrids",
|
|
16
|
+
"healthybowl"
|
|
17
|
+
], p = () => {
|
|
18
|
+
let p = i(), m = n(), [h] = r(), [g, _] = a(!1), [v, y] = a({}), b = (/* @__PURE__ */ new Date()).toISOString().split("T")[0], [x, S] = a({
|
|
19
|
+
name: "",
|
|
20
|
+
description: "",
|
|
21
|
+
startsAt: b,
|
|
22
|
+
expiresAt: "",
|
|
23
|
+
bonusMonths: 1,
|
|
24
|
+
maxRedemptions: "",
|
|
25
|
+
applicableProductIds: [],
|
|
26
|
+
applicablePlanIds: ""
|
|
27
|
+
});
|
|
28
|
+
if (!m.canManagePlans) return /* @__PURE__ */ u(e, { message: "You don't have permission to create promotions." });
|
|
29
|
+
let C = (e, t) => S((n) => ({
|
|
30
|
+
...n,
|
|
31
|
+
[e]: t
|
|
32
|
+
})), w = (e) => {
|
|
33
|
+
C("applicableProductIds", x.applicableProductIds.includes(e) ? x.applicableProductIds.filter((t) => t !== e) : [...x.applicableProductIds, e]);
|
|
34
|
+
}, T = () => {
|
|
35
|
+
let e = {};
|
|
36
|
+
return x.name.trim() || (e.name = "Name is required"), x.startsAt || (e.startsAt = "Start date is required"), x.expiresAt || (e.expiresAt = "End date is required"), x.expiresAt && x.expiresAt <= x.startsAt && (e.expiresAt = "End date must be after start date"), x.bonusMonths < 1 && (e.bonusMonths = "Must be at least 1"), y(e), Object.keys(e).length === 0;
|
|
37
|
+
};
|
|
38
|
+
return /* @__PURE__ */ d("div", {
|
|
39
|
+
className: "max-w-2xl mx-auto p-4 sm:p-6 space-y-6",
|
|
40
|
+
children: [/* @__PURE__ */ d("div", {
|
|
41
|
+
className: "flex items-center gap-4",
|
|
42
|
+
children: [/* @__PURE__ */ u("button", {
|
|
43
|
+
type: "button",
|
|
44
|
+
onClick: () => p("/promotions"),
|
|
45
|
+
className: "p-2 rounded-lg hover:bg-muted text-muted-foreground",
|
|
46
|
+
children: /* @__PURE__ */ u(o, { className: "size-5" })
|
|
47
|
+
}), /* @__PURE__ */ u(t, {
|
|
48
|
+
title: "Create Promotion",
|
|
49
|
+
description: "Offer bonus free renewal months to subscribers who sign up within the promotion window",
|
|
50
|
+
icon: /* @__PURE__ */ u(s, { className: "size-6" })
|
|
51
|
+
})]
|
|
52
|
+
}), /* @__PURE__ */ d("form", {
|
|
53
|
+
onSubmit: async (e) => {
|
|
54
|
+
if (e.preventDefault(), T()) {
|
|
55
|
+
_(!0);
|
|
56
|
+
try {
|
|
57
|
+
let e = x.applicablePlanIds.split(",").map((e) => e.trim()).filter(Boolean);
|
|
58
|
+
await h({ variables: { input: {
|
|
59
|
+
name: x.name.trim(),
|
|
60
|
+
description: x.description.trim() || void 0,
|
|
61
|
+
startsAt: new Date(x.startsAt).toISOString(),
|
|
62
|
+
expiresAt: new Date(x.expiresAt).toISOString(),
|
|
63
|
+
bonusMonths: x.bonusMonths,
|
|
64
|
+
maxRedemptions: x.maxRedemptions ? parseInt(x.maxRedemptions) : void 0,
|
|
65
|
+
applicableProductIds: x.applicableProductIds,
|
|
66
|
+
applicablePlanIds: e
|
|
67
|
+
} } }), p("/promotions");
|
|
68
|
+
} catch (e) {
|
|
69
|
+
y({ name: e instanceof Error ? e.message : "Failed to create promotion" });
|
|
70
|
+
} finally {
|
|
71
|
+
_(!1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
className: "space-y-5",
|
|
76
|
+
children: [
|
|
77
|
+
/* @__PURE__ */ d("div", {
|
|
78
|
+
className: "bg-card border border-border rounded-xl p-5 space-y-4",
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ u("h3", {
|
|
81
|
+
className: "font-semibold text-foreground",
|
|
82
|
+
children: "Promotion Details"
|
|
83
|
+
}),
|
|
84
|
+
/* @__PURE__ */ d("div", { children: [
|
|
85
|
+
/* @__PURE__ */ u("label", {
|
|
86
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
87
|
+
children: "Name *"
|
|
88
|
+
}),
|
|
89
|
+
/* @__PURE__ */ u("input", {
|
|
90
|
+
type: "text",
|
|
91
|
+
value: x.name,
|
|
92
|
+
onChange: (e) => C("name", e.target.value),
|
|
93
|
+
placeholder: "e.g. June Launch Offer",
|
|
94
|
+
className: `w-full px-3 py-2 rounded-lg border ${v.name ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
|
|
95
|
+
}),
|
|
96
|
+
v.name && /* @__PURE__ */ u("p", {
|
|
97
|
+
className: "text-xs text-destructive mt-1",
|
|
98
|
+
children: v.name
|
|
99
|
+
})
|
|
100
|
+
] }),
|
|
101
|
+
/* @__PURE__ */ d("div", { children: [/* @__PURE__ */ u("label", {
|
|
102
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
103
|
+
children: "Description"
|
|
104
|
+
}), /* @__PURE__ */ u("textarea", {
|
|
105
|
+
value: x.description,
|
|
106
|
+
onChange: (e) => C("description", e.target.value),
|
|
107
|
+
placeholder: "Subscribe in June and get 2 extra months free!",
|
|
108
|
+
rows: 2,
|
|
109
|
+
className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
110
|
+
})] })
|
|
111
|
+
]
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ d("div", {
|
|
114
|
+
className: "bg-card border border-border rounded-xl p-5 space-y-4",
|
|
115
|
+
children: [
|
|
116
|
+
/* @__PURE__ */ u("h3", {
|
|
117
|
+
className: "font-semibold text-foreground",
|
|
118
|
+
children: "Bonus Configuration"
|
|
119
|
+
}),
|
|
120
|
+
/* @__PURE__ */ d("div", { children: [
|
|
121
|
+
/* @__PURE__ */ u("label", {
|
|
122
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
123
|
+
children: "Bonus Months *"
|
|
124
|
+
}),
|
|
125
|
+
/* @__PURE__ */ u("input", {
|
|
126
|
+
type: "number",
|
|
127
|
+
value: x.bonusMonths,
|
|
128
|
+
min: 1,
|
|
129
|
+
max: 12,
|
|
130
|
+
onChange: (e) => C("bonusMonths", parseInt(e.target.value) || 1),
|
|
131
|
+
className: `w-32 px-3 py-2 rounded-lg border ${v.bonusMonths ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
|
|
132
|
+
}),
|
|
133
|
+
v.bonusMonths && /* @__PURE__ */ u("p", {
|
|
134
|
+
className: "text-xs text-destructive mt-1",
|
|
135
|
+
children: v.bonusMonths
|
|
136
|
+
}),
|
|
137
|
+
/* @__PURE__ */ d("div", {
|
|
138
|
+
className: "mt-2 flex items-start gap-2 p-3 bg-primary/5 rounded-lg text-sm text-muted-foreground",
|
|
139
|
+
children: [/* @__PURE__ */ u(c, { className: "size-4 shrink-0 mt-0.5 text-primary" }), /* @__PURE__ */ d("span", { children: [
|
|
140
|
+
"Users who subscribe during this window get",
|
|
141
|
+
" ",
|
|
142
|
+
/* @__PURE__ */ d("strong", {
|
|
143
|
+
className: "text-foreground",
|
|
144
|
+
children: [
|
|
145
|
+
x.bonusMonths,
|
|
146
|
+
" free renewal",
|
|
147
|
+
x.bonusMonths === 1 ? "" : "s"
|
|
148
|
+
]
|
|
149
|
+
}),
|
|
150
|
+
" ",
|
|
151
|
+
"after their first paid period — their quota resets each month as normal."
|
|
152
|
+
] })]
|
|
153
|
+
})
|
|
154
|
+
] }),
|
|
155
|
+
/* @__PURE__ */ d("div", { children: [
|
|
156
|
+
/* @__PURE__ */ u("label", {
|
|
157
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
158
|
+
children: "Max Redemptions"
|
|
159
|
+
}),
|
|
160
|
+
/* @__PURE__ */ u("input", {
|
|
161
|
+
type: "number",
|
|
162
|
+
value: x.maxRedemptions,
|
|
163
|
+
min: 1,
|
|
164
|
+
onChange: (e) => C("maxRedemptions", e.target.value),
|
|
165
|
+
placeholder: "Unlimited",
|
|
166
|
+
className: "w-48 px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
167
|
+
}),
|
|
168
|
+
/* @__PURE__ */ u("p", {
|
|
169
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
170
|
+
children: "Leave empty for no cap"
|
|
171
|
+
})
|
|
172
|
+
] })
|
|
173
|
+
]
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ d("div", {
|
|
176
|
+
className: "bg-card border border-border rounded-xl p-5 space-y-4",
|
|
177
|
+
children: [
|
|
178
|
+
/* @__PURE__ */ u("h3", {
|
|
179
|
+
className: "font-semibold text-foreground",
|
|
180
|
+
children: "Active Window"
|
|
181
|
+
}),
|
|
182
|
+
/* @__PURE__ */ d("div", {
|
|
183
|
+
className: "grid grid-cols-2 gap-4",
|
|
184
|
+
children: [/* @__PURE__ */ d("div", { children: [
|
|
185
|
+
/* @__PURE__ */ u("label", {
|
|
186
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
187
|
+
children: "Start Date *"
|
|
188
|
+
}),
|
|
189
|
+
/* @__PURE__ */ u("input", {
|
|
190
|
+
type: "date",
|
|
191
|
+
value: x.startsAt,
|
|
192
|
+
onChange: (e) => C("startsAt", e.target.value),
|
|
193
|
+
className: `w-full px-3 py-2 rounded-lg border ${v.startsAt ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
|
|
194
|
+
}),
|
|
195
|
+
v.startsAt && /* @__PURE__ */ u("p", {
|
|
196
|
+
className: "text-xs text-destructive mt-1",
|
|
197
|
+
children: v.startsAt
|
|
198
|
+
})
|
|
199
|
+
] }), /* @__PURE__ */ d("div", { children: [
|
|
200
|
+
/* @__PURE__ */ u("label", {
|
|
201
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
202
|
+
children: "End Date *"
|
|
203
|
+
}),
|
|
204
|
+
/* @__PURE__ */ u("input", {
|
|
205
|
+
type: "date",
|
|
206
|
+
value: x.expiresAt,
|
|
207
|
+
onChange: (e) => C("expiresAt", e.target.value),
|
|
208
|
+
min: x.startsAt,
|
|
209
|
+
className: `w-full px-3 py-2 rounded-lg border ${v.expiresAt ? "border-destructive" : "border-border"} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`
|
|
210
|
+
}),
|
|
211
|
+
v.expiresAt && /* @__PURE__ */ u("p", {
|
|
212
|
+
className: "text-xs text-destructive mt-1",
|
|
213
|
+
children: v.expiresAt
|
|
214
|
+
})
|
|
215
|
+
] })]
|
|
216
|
+
}),
|
|
217
|
+
/* @__PURE__ */ u("p", {
|
|
218
|
+
className: "text-xs text-muted-foreground",
|
|
219
|
+
children: "Only purchases made between these dates will receive the bonus months"
|
|
220
|
+
})
|
|
221
|
+
]
|
|
222
|
+
}),
|
|
223
|
+
/* @__PURE__ */ d("div", {
|
|
224
|
+
className: "bg-card border border-border rounded-xl p-5 space-y-4",
|
|
225
|
+
children: [
|
|
226
|
+
/* @__PURE__ */ u("h3", {
|
|
227
|
+
className: "font-semibold text-foreground",
|
|
228
|
+
children: "Scope"
|
|
229
|
+
}),
|
|
230
|
+
/* @__PURE__ */ d("div", { children: [
|
|
231
|
+
/* @__PURE__ */ u("label", {
|
|
232
|
+
className: "block text-sm font-medium text-foreground mb-2",
|
|
233
|
+
children: "Applicable Products"
|
|
234
|
+
}),
|
|
235
|
+
/* @__PURE__ */ u("div", {
|
|
236
|
+
className: "flex flex-wrap gap-2",
|
|
237
|
+
children: f.map((e) => /* @__PURE__ */ u("button", {
|
|
238
|
+
type: "button",
|
|
239
|
+
onClick: () => w(e),
|
|
240
|
+
className: `px-3 py-1.5 rounded-lg border text-sm transition-colors ${x.applicableProductIds.includes(e) ? "border-primary bg-primary/10 text-primary font-medium" : "border-border hover:border-primary/50"}`,
|
|
241
|
+
children: e
|
|
242
|
+
}, e))
|
|
243
|
+
}),
|
|
244
|
+
/* @__PURE__ */ u("p", {
|
|
245
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
246
|
+
children: x.applicableProductIds.length === 0 ? "No selection = applies to all products" : `Applies to: ${x.applicableProductIds.join(", ")}`
|
|
247
|
+
})
|
|
248
|
+
] }),
|
|
249
|
+
/* @__PURE__ */ d("div", { children: [
|
|
250
|
+
/* @__PURE__ */ u("label", {
|
|
251
|
+
className: "block text-sm font-medium text-foreground mb-1",
|
|
252
|
+
children: "Plan IDs (optional)"
|
|
253
|
+
}),
|
|
254
|
+
/* @__PURE__ */ u("input", {
|
|
255
|
+
type: "text",
|
|
256
|
+
value: x.applicablePlanIds,
|
|
257
|
+
onChange: (e) => C("applicablePlanIds", e.target.value),
|
|
258
|
+
placeholder: "plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly",
|
|
259
|
+
className: "w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20"
|
|
260
|
+
}),
|
|
261
|
+
/* @__PURE__ */ u("p", {
|
|
262
|
+
className: "text-xs text-muted-foreground mt-1",
|
|
263
|
+
children: "Comma-separated. Leave empty to apply to all plans"
|
|
264
|
+
})
|
|
265
|
+
] })
|
|
266
|
+
]
|
|
267
|
+
}),
|
|
268
|
+
/* @__PURE__ */ d("div", {
|
|
269
|
+
className: "flex gap-3 justify-end",
|
|
270
|
+
children: [/* @__PURE__ */ u("button", {
|
|
271
|
+
type: "button",
|
|
272
|
+
onClick: () => p("/promotions"),
|
|
273
|
+
className: "px-5 py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm",
|
|
274
|
+
children: "Cancel"
|
|
275
|
+
}), /* @__PURE__ */ d("button", {
|
|
276
|
+
type: "submit",
|
|
277
|
+
disabled: g,
|
|
278
|
+
className: "px-5 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 text-sm disabled:opacity-50",
|
|
279
|
+
children: [g ? /* @__PURE__ */ u("div", { className: "size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin" }) : /* @__PURE__ */ u(l, { className: "size-4" }), "Create Promotion"]
|
|
280
|
+
})]
|
|
281
|
+
})
|
|
282
|
+
]
|
|
283
|
+
})]
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
//#endregion
|
|
287
|
+
export { p as CreatePromotionPage };
|
|
288
|
+
|
|
289
|
+
//# sourceMappingURL=CreatePromotionPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CreatePromotionPage.js","names":[],"sources":["../../../../../src/billing/modules/promotions/pages/CreatePromotionPage.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { ArrowLeft, Save, Gift, Info } from 'lucide-react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { useCreatePromotionMutation } from '../../../../generated/global-operations';\n\nconst KNOWN_PRODUCTS = ['vibecontrols', 'workspaces', 'botlit', 'fluidgrids', 'healthybowl'];\n\ninterface FormData {\n name: string;\n description: string;\n startsAt: string;\n expiresAt: string;\n bonusMonths: number;\n maxRedemptions: string;\n applicableProductIds: string[];\n applicablePlanIds: string;\n}\n\nexport const CreatePromotionPage: FC = () => {\n const navigateTo = useBillingNavigate();\n const permissions = useBillingPermissions();\n const [createPromotion] = useCreatePromotionMutation();\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});\n\n const today = new Date().toISOString().split('T')[0];\n const [form, setForm] = useState<FormData>({\n name: '',\n description: '',\n startsAt: today,\n expiresAt: '',\n bonusMonths: 1,\n maxRedemptions: '',\n applicableProductIds: [],\n applicablePlanIds: '',\n });\n\n if (!permissions.canManagePlans) {\n return <AccessDenied message=\"You don't have permission to create promotions.\" />;\n }\n\n const set = <K extends keyof FormData>(k: K, v: FormData[K]) =>\n setForm((prev) => ({ ...prev, [k]: v }));\n\n const toggleProduct = (pid: string) => {\n set(\n 'applicableProductIds',\n form.applicableProductIds.includes(pid)\n ? form.applicableProductIds.filter((p) => p !== pid)\n : [...form.applicableProductIds, pid]\n );\n };\n\n const validate = () => {\n const e: typeof errors = {};\n if (!form.name.trim()) e.name = 'Name is required';\n if (!form.startsAt) e.startsAt = 'Start date is required';\n if (!form.expiresAt) e.expiresAt = 'End date is required';\n if (form.expiresAt && form.expiresAt <= form.startsAt)\n e.expiresAt = 'End date must be after start date';\n if (form.bonusMonths < 1) e.bonusMonths = 'Must be at least 1';\n setErrors(e);\n return Object.keys(e).length === 0;\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n setIsSubmitting(true);\n try {\n const planIds = form.applicablePlanIds\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n await createPromotion({\n variables: {\n input: {\n name: form.name.trim(),\n description: form.description.trim() || undefined,\n startsAt: new Date(form.startsAt).toISOString(),\n expiresAt: new Date(form.expiresAt).toISOString(),\n bonusMonths: form.bonusMonths,\n maxRedemptions: form.maxRedemptions ? parseInt(form.maxRedemptions) : undefined,\n applicableProductIds: form.applicableProductIds,\n applicablePlanIds: planIds,\n },\n },\n });\n navigateTo('/promotions');\n } catch (err) {\n setErrors({ name: err instanceof Error ? err.message : 'Failed to create promotion' });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"max-w-2xl mx-auto p-4 sm:p-6 space-y-6\">\n <div className=\"flex items-center gap-4\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"p-2 rounded-lg hover:bg-muted text-muted-foreground\"\n >\n <ArrowLeft className=\"size-5\" />\n </button>\n <PageHeader\n title=\"Create Promotion\"\n description=\"Offer bonus free renewal months to subscribers who sign up within the promotion window\"\n icon={<Gift className=\"size-6\" />}\n />\n </div>\n\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Basic info */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Promotion Details</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Name *</label>\n <input\n type=\"text\"\n value={form.name}\n onChange={(e) => set('name', e.target.value)}\n placeholder=\"e.g. June Launch Offer\"\n className={`w-full px-3 py-2 rounded-lg border ${errors.name ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.name && <p className=\"text-xs text-destructive mt-1\">{errors.name}</p>}\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Description</label>\n <textarea\n value={form.description}\n onChange={(e) => set('description', e.target.value)}\n placeholder=\"Subscribe in June and get 2 extra months free!\"\n rows={2}\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n </div>\n </div>\n\n {/* Bonus config */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Bonus Configuration</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Bonus Months *</label>\n <input\n type=\"number\"\n value={form.bonusMonths}\n min={1}\n max={12}\n onChange={(e) => set('bonusMonths', parseInt(e.target.value) || 1)}\n className={`w-32 px-3 py-2 rounded-lg border ${errors.bonusMonths ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.bonusMonths && (\n <p className=\"text-xs text-destructive mt-1\">{errors.bonusMonths}</p>\n )}\n <div className=\"mt-2 flex items-start gap-2 p-3 bg-primary/5 rounded-lg text-sm text-muted-foreground\">\n <Info className=\"size-4 shrink-0 mt-0.5 text-primary\" />\n <span>\n Users who subscribe during this window get{' '}\n <strong className=\"text-foreground\">\n {form.bonusMonths} free renewal{form.bonusMonths !== 1 ? 's' : ''}\n </strong>{' '}\n after their first paid period — their quota resets each month as normal.\n </span>\n </div>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Max Redemptions\n </label>\n <input\n type=\"number\"\n value={form.maxRedemptions}\n min={1}\n onChange={(e) => set('maxRedemptions', e.target.value)}\n placeholder=\"Unlimited\"\n className=\"w-48 px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">Leave empty for no cap</p>\n </div>\n </div>\n\n {/* Date window */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Active Window</h3>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">Start Date *</label>\n <input\n type=\"date\"\n value={form.startsAt}\n onChange={(e) => set('startsAt', e.target.value)}\n className={`w-full px-3 py-2 rounded-lg border ${errors.startsAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.startsAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.startsAt}</p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">End Date *</label>\n <input\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => set('expiresAt', e.target.value)}\n min={form.startsAt}\n className={`w-full px-3 py-2 rounded-lg border ${errors.expiresAt ? 'border-destructive' : 'border-border'} bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20`}\n />\n {errors.expiresAt && (\n <p className=\"text-xs text-destructive mt-1\">{errors.expiresAt}</p>\n )}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n Only purchases made between these dates will receive the bonus months\n </p>\n </div>\n\n {/* Scope */}\n <div className=\"bg-card border border-border rounded-xl p-5 space-y-4\">\n <h3 className=\"font-semibold text-foreground\">Scope</h3>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-2\">\n Applicable Products\n </label>\n <div className=\"flex flex-wrap gap-2\">\n {KNOWN_PRODUCTS.map((pid) => (\n <button\n key={pid}\n type=\"button\"\n onClick={() => toggleProduct(pid)}\n className={`px-3 py-1.5 rounded-lg border text-sm transition-colors ${\n form.applicableProductIds.includes(pid)\n ? 'border-primary bg-primary/10 text-primary font-medium'\n : 'border-border hover:border-primary/50'\n }`}\n >\n {pid}\n </button>\n ))}\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {form.applicableProductIds.length === 0\n ? 'No selection = applies to all products'\n : `Applies to: ${form.applicableProductIds.join(', ')}`}\n </p>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-foreground mb-1\">\n Plan IDs (optional)\n </label>\n <input\n type=\"text\"\n value={form.applicablePlanIds}\n onChange={(e) => set('applicablePlanIds', e.target.value)}\n placeholder=\"plan-vibecontrols-pro-monthly, plan-vibecontrols-pro-yearly\"\n className=\"w-full px-3 py-2 rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground text-sm focus:outline-none focus:ring-2 focus:ring-primary/20\"\n />\n <p className=\"text-xs text-muted-foreground mt-1\">\n Comma-separated. Leave empty to apply to all plans\n </p>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-3 justify-end\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/promotions')}\n className=\"px-5 py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={isSubmitting}\n className=\"px-5 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 text-sm disabled:opacity-50\"\n >\n {isSubmitting ? (\n <div className=\"size-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin\" />\n ) : (\n <Save className=\"size-4\" />\n )}\n Create Promotion\n </button>\n </div>\n </form>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,168 @@
|
|
|
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 { useDeactivatePromotionMutation as r, useListPromotionsQuery as i, useUpdatePromotionMutation as a } from "../../../../generated/global-operations.js";
|
|
6
|
+
import { useBillingNavigate as o } from "../../../hooks/useBillingNavigate.js";
|
|
7
|
+
import { useState as s } from "react";
|
|
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
|
+
import { jsx as h, jsxs as g } from "react/jsx-runtime";
|
|
10
|
+
//#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);
|
|
16
|
+
try {
|
|
17
|
+
t ? await E({ variables: { id: e } }) : await D({ variables: {
|
|
18
|
+
id: e,
|
|
19
|
+
input: { isActive: !0 }
|
|
20
|
+
} }), await T();
|
|
21
|
+
} finally {
|
|
22
|
+
S(null);
|
|
23
|
+
}
|
|
24
|
+
}, A = /* @__PURE__ */ new Date(), j = (e) => e.isActive ? new Date(e.expiresAt) < A ? {
|
|
25
|
+
text: "Expired",
|
|
26
|
+
cls: "bg-destructive/10 text-destructive"
|
|
27
|
+
} : new Date(e.startsAt) > A ? {
|
|
28
|
+
text: "Upcoming",
|
|
29
|
+
cls: "bg-primary/10 text-primary"
|
|
30
|
+
} : {
|
|
31
|
+
text: "Live",
|
|
32
|
+
cls: "bg-green-500/10 text-green-600"
|
|
33
|
+
} : {
|
|
34
|
+
text: "Inactive",
|
|
35
|
+
cls: "bg-muted text-muted-foreground"
|
|
36
|
+
};
|
|
37
|
+
return /* @__PURE__ */ g("div", {
|
|
38
|
+
className: "max-w-5xl mx-auto p-4 sm:p-6 space-y-6",
|
|
39
|
+
children: [
|
|
40
|
+
/* @__PURE__ */ g("div", {
|
|
41
|
+
className: "flex items-center justify-between gap-4",
|
|
42
|
+
children: [/* @__PURE__ */ h(t, {
|
|
43
|
+
title: "Promotions",
|
|
44
|
+
description: "Time-bounded offers that grant bonus free renewal months to subscribers",
|
|
45
|
+
icon: /* @__PURE__ */ h(l, { className: "size-6" })
|
|
46
|
+
}), /* @__PURE__ */ g("button", {
|
|
47
|
+
onClick: () => _("/promotions/create"),
|
|
48
|
+
className: "flex items-center gap-2 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 text-sm font-medium shrink-0",
|
|
49
|
+
children: [/* @__PURE__ */ h(u, { className: "size-4" }), "New Promotion"]
|
|
50
|
+
})]
|
|
51
|
+
}),
|
|
52
|
+
/* @__PURE__ */ h("div", {
|
|
53
|
+
className: "flex items-center gap-3",
|
|
54
|
+
children: /* @__PURE__ */ g("label", {
|
|
55
|
+
className: "flex items-center gap-2 text-sm text-muted-foreground cursor-pointer select-none",
|
|
56
|
+
children: [/* @__PURE__ */ h("input", {
|
|
57
|
+
type: "checkbox",
|
|
58
|
+
checked: y,
|
|
59
|
+
onChange: (e) => b(e.target.checked),
|
|
60
|
+
className: "rounded border-border"
|
|
61
|
+
}), "Show inactive / expired"]
|
|
62
|
+
})
|
|
63
|
+
}),
|
|
64
|
+
w ? /* @__PURE__ */ h("div", {
|
|
65
|
+
className: "text-center py-16 text-muted-foreground",
|
|
66
|
+
children: "Loading promotions…"
|
|
67
|
+
}) : O.length === 0 ? /* @__PURE__ */ g("div", {
|
|
68
|
+
className: "text-center py-16 border border-dashed border-border rounded-xl",
|
|
69
|
+
children: [
|
|
70
|
+
/* @__PURE__ */ h(l, { className: "size-10 mx-auto mb-3 text-muted-foreground/50" }),
|
|
71
|
+
/* @__PURE__ */ h("p", {
|
|
72
|
+
className: "text-muted-foreground font-medium",
|
|
73
|
+
children: "No promotions yet"
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ h("p", {
|
|
76
|
+
className: "text-sm text-muted-foreground mt-1",
|
|
77
|
+
children: "Create one to offer bonus months to early subscribers"
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ h("button", {
|
|
80
|
+
onClick: () => _("/promotions/create"),
|
|
81
|
+
className: "mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm",
|
|
82
|
+
children: "Create Promotion"
|
|
83
|
+
})
|
|
84
|
+
]
|
|
85
|
+
}) : /* @__PURE__ */ h("div", {
|
|
86
|
+
className: "space-y-3",
|
|
87
|
+
children: O.map((e) => {
|
|
88
|
+
let { text: t, cls: n } = j(e), r = x === e.id;
|
|
89
|
+
return /* @__PURE__ */ g("div", {
|
|
90
|
+
className: "bg-card border border-border rounded-xl p-5 flex flex-col sm:flex-row sm:items-center gap-4",
|
|
91
|
+
children: [/* @__PURE__ */ g("div", {
|
|
92
|
+
className: "flex-1 min-w-0",
|
|
93
|
+
children: [
|
|
94
|
+
/* @__PURE__ */ g("div", {
|
|
95
|
+
className: "flex items-center gap-2 mb-1",
|
|
96
|
+
children: [/* @__PURE__ */ h("span", {
|
|
97
|
+
className: "font-semibold text-foreground truncate",
|
|
98
|
+
children: e.name
|
|
99
|
+
}), /* @__PURE__ */ h("span", {
|
|
100
|
+
className: `text-xs font-medium px-2 py-0.5 rounded-full ${n}`,
|
|
101
|
+
children: t
|
|
102
|
+
})]
|
|
103
|
+
}),
|
|
104
|
+
e.description && /* @__PURE__ */ h("p", {
|
|
105
|
+
className: "text-sm text-muted-foreground mb-2 line-clamp-1",
|
|
106
|
+
children: e.description
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ g("div", {
|
|
109
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground",
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ g("span", {
|
|
112
|
+
className: "flex items-center gap-1",
|
|
113
|
+
children: [
|
|
114
|
+
/* @__PURE__ */ h(l, { className: "size-3" }),
|
|
115
|
+
/* @__PURE__ */ h("strong", {
|
|
116
|
+
className: "text-foreground",
|
|
117
|
+
children: e.bonusMonths
|
|
118
|
+
}),
|
|
119
|
+
" bonus month",
|
|
120
|
+
e.bonusMonths === 1 ? "" : "s"
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
/* @__PURE__ */ g("span", {
|
|
124
|
+
className: "flex items-center gap-1",
|
|
125
|
+
children: [
|
|
126
|
+
/* @__PURE__ */ h(c, { className: "size-3" }),
|
|
127
|
+
new Date(e.startsAt).toLocaleDateString(),
|
|
128
|
+
" –",
|
|
129
|
+
" ",
|
|
130
|
+
new Date(e.expiresAt).toLocaleDateString()
|
|
131
|
+
]
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ g("span", {
|
|
134
|
+
className: "flex items-center gap-1",
|
|
135
|
+
children: [
|
|
136
|
+
/* @__PURE__ */ h(m, { className: "size-3" }),
|
|
137
|
+
e.currentRedemptions,
|
|
138
|
+
e.maxRedemptions ? ` / ${e.maxRedemptions}` : "",
|
|
139
|
+
" redemptions"
|
|
140
|
+
]
|
|
141
|
+
}),
|
|
142
|
+
e.applicableProductIds.length > 0 && /* @__PURE__ */ g("span", {
|
|
143
|
+
className: "flex items-center gap-1",
|
|
144
|
+
children: [/* @__PURE__ */ h(d, { className: "size-3" }), e.applicableProductIds.join(", ")]
|
|
145
|
+
})
|
|
146
|
+
]
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
}), /* @__PURE__ */ h("div", {
|
|
150
|
+
className: "flex items-center gap-2 shrink-0",
|
|
151
|
+
children: /* @__PURE__ */ h("button", {
|
|
152
|
+
onClick: () => k(e.id, e.isActive),
|
|
153
|
+
disabled: r,
|
|
154
|
+
title: e.isActive ? "Deactivate" : "Reactivate",
|
|
155
|
+
className: `p-2 rounded-lg transition-colors ${e.isActive ? "text-green-600 hover:bg-green-50" : "text-muted-foreground hover:bg-muted"} disabled:opacity-50`,
|
|
156
|
+
children: e.isActive ? /* @__PURE__ */ h(p, { className: "size-5" }) : /* @__PURE__ */ h(f, { className: "size-5" })
|
|
157
|
+
})
|
|
158
|
+
})]
|
|
159
|
+
}, e.id);
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
export { _ as PromotionsListPage };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=PromotionsListPage.js.map
|