@burdenoff/microfe-billing 2026.624.3 → 2026.625.2
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 +1 -1
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonCreatePage.js +2 -2
- package/dist/billing/modules/addons/pages/AddonCreatePage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonDetailPage.js +4 -4
- package/dist/billing/modules/addons/pages/AddonDetailPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonEditPage.js +2 -2
- package/dist/billing/modules/addons/pages/AddonEditPage.js.map +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js +1 -1
- package/dist/billing/modules/addons/pages/AddonsListPage.js.map +1 -1
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js +98 -99
- package/dist/billing/modules/affiliate/pages/AffiliateDashboardPage.js.map +1 -1
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js +88 -85
- package/dist/billing/modules/analytics/pages/AnalyticsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js +6 -6
- package/dist/billing/modules/billing/pages/InvoiceDetailPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/InvoicesListPage.js +2 -2
- package/dist/billing/modules/billing/pages/InvoicesListPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundsPage.js +360 -370
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/UserRefundsPage.js +2 -2
- package/dist/billing/modules/billing/pages/UserRefundsPage.js.map +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js +1 -1
- package/dist/billing/modules/checkout/components/BillingAccountSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js +1 -1
- package/dist/billing/modules/checkout/components/CompleteBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js +1 -1
- package/dist/billing/modules/checkout/components/SubscriptionSelector.js.map +1 -1
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js +2 -2
- package/dist/billing/modules/checkout/components/WorkspaceQuotaAssignmentModal.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js +102 -104
- package/dist/billing/modules/coupons/pages/CouponDetailPage.js.map +1 -1
- package/dist/billing/modules/coupons/pages/CouponsListPage.js +5 -5
- package/dist/billing/modules/coupons/pages/CouponsListPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js +1 -1
- package/dist/billing/modules/credits/pages/AdminCreditsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js +1 -1
- package/dist/billing/modules/credits/pages/CreditTransactionsPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditsPage.js +29 -28
- package/dist/billing/modules/credits/pages/CreditsPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js +2 -2
- package/dist/billing/modules/dashboard/components/ActiveSubscriptionCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js +3 -3
- package/dist/billing/modules/dashboard/components/BillingAccountCard.js.map +1 -1
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js +2 -2
- package/dist/billing/modules/dashboard/components/CreateBillingAccountModal.js.map +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js +1 -1
- package/dist/billing/modules/dashboard/components/PaymentMethodsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js +1 -1
- package/dist/billing/modules/dashboard/components/RecommendedAddonsSection.js.map +1 -1
- package/dist/billing/modules/dashboard/pages/OverviewPage.js +139 -142
- package/dist/billing/modules/dashboard/pages/OverviewPage.js.map +1 -1
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js +6 -6
- package/dist/billing/modules/discounts/pages/DiscountsListPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +4 -4
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js +1 -1
- package/dist/billing/modules/plans/pages/PlanAssignFreePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js +142 -133
- package/dist/billing/modules/plans/pages/PlanBrowseDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanCreatePage.js +2 -2
- package/dist/billing/modules/plans/pages/PlanCreatePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanDetailPage.js +4 -4
- package/dist/billing/modules/plans/pages/PlanDetailPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlanEditPage.js +2 -2
- package/dist/billing/modules/plans/pages/PlanEditPage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js +4 -4
- package/dist/billing/modules/plans/pages/PlansBrowsePage.js.map +1 -1
- package/dist/billing/modules/plans/pages/PlansListPage.js +2 -2
- package/dist/billing/modules/plans/pages/PlansListPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js +4 -4
- package/dist/billing/modules/promotions/pages/CreatePromotionPage.js.map +1 -1
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js +2 -2
- package/dist/billing/modules/promotions/pages/PromotionsListPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js +2 -2
- package/dist/billing/modules/quota/pages/QuotaTemplateCreatePage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js +5 -5
- package/dist/billing/modules/quota/pages/QuotaTemplateDetailPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js +2 -2
- package/dist/billing/modules/quota/pages/QuotaTemplateEditPage.js.map +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js +1 -1
- package/dist/billing/modules/quota/pages/QuotaTemplatesListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +259 -265
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +7 -7
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/usage/pages/UsagePage.js +13 -13
- package/dist/billing/modules/usage/pages/UsagePage.js.map +1 -1
- package/dist/billing/shared/ui/Card.js +1 -1
- package/dist/billing/shared/ui/Card.js.map +1 -1
- package/dist/billing/shared/ui/MetricCard.js +28 -21
- package/dist/billing/shared/ui/MetricCard.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CouponsListPage.js","names":[],"sources":["../../../../../src/billing/modules/coupons/pages/CouponsListPage.tsx"],"sourcesContent":["/**\n * Coupons List Page\n *\n * Admin interface for managing coupon codes:\n * - View all coupons\n * - Filter by status, type, scope\n * - Create new coupons\n * - Edit/deactivate existing coupons\n * - View usage analytics\n */\n\nimport { type FC, useState, useMemo, useEffect } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport {\n Plus,\n Search,\n Tag,\n TrendingUp,\n CheckCircle,\n AlertCircle,\n Percent,\n DollarSign,\n Package,\n Eye,\n Edit,\n Trash2,\n Download,\n PowerOff,\n Power,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\n\nimport { ResponsiveTable, type ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListCouponsQuery,\n useDeactivateCouponMutation,\n useUpdateCouponMutation,\n} from '../../../../generated/global-operations';\nimport { CouponStatus } from '../../../../generated/global-types';\nimport type {\n CouponScope as GQLCouponScope,\n CouponStatus as GQLCouponStatus,\n CouponType as GQLCouponType,\n Coupon as GQLCoupon,\n} from '../../../../generated/global-types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype Coupon = GQLCoupon;\n\ntype CouponStatusFilter = 'ALL' | GQLCouponStatus;\ntype CouponTypeFilter = 'ALL' | GQLCouponType;\ntype CouponScopeFilter = 'ALL' | GQLCouponScope;\n\ninterface CouponExportRow {\n code: string;\n name: string;\n description: string;\n status: string;\n type: string;\n value: number;\n currentUses: number;\n maxTotalUses: number | '';\n validUntil: string;\n}\n\nexport const CouponsListPage: 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 PAGE_SIZE = 20;\n\n // State\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState<CouponStatusFilter>('ALL');\n const [typeFilter, setTypeFilter] = useState<CouponTypeFilter>('ALL');\n const [scopeFilter] = useState<CouponScopeFilter>('ALL');\n const [page, setPage] = useState(0);\n const [couponToDeactivate, setCouponToDeactivate] = useState<Coupon | null>(null);\n const [togglingId, setTogglingId] = useState<string | null>(null);\n const [feedback, setFeedback] = useState<{\n tone: 'success' | 'error';\n message: string;\n } | null>(null);\n\n // Fetch coupons from backend (server-side pagination)\n const { data, loading, refetch } = useListCouponsQuery({\n variables: {\n limit: PAGE_SIZE,\n offset: page * PAGE_SIZE,\n filter: {\n status: statusFilter !== 'ALL' ? (statusFilter as GQLCouponStatus) : undefined,\n type: typeFilter !== 'ALL' ? (typeFilter as GQLCouponType) : undefined,\n scope: scopeFilter !== 'ALL' ? (scopeFilter as GQLCouponScope) : undefined,\n search: searchQuery || undefined,\n },\n },\n });\n\n const coupons = useMemo<Coupon[]>(\n () => data?.listCoupons?.coupons ?? [],\n [data?.listCoupons?.coupons]\n );\n const total = data?.listCoupons?.total ?? 0;\n const hasMore = data?.listCoupons?.hasMore ?? false;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n // Mutations\n const [deactivateCoupon, { loading: deactivating }] = useDeactivateCouponMutation();\n const [updateCoupon] = useUpdateCouponMutation();\n\n // With server-side filtering, filteredCoupons === coupons\n const filteredCoupons = coupons;\n\n // Reset to first page when filters change\n useEffect(() => {\n setPage(0);\n }, [searchQuery, statusFilter, typeFilter, scopeFilter]);\n\n // Statistics from current page (best effort without a separate aggregation query)\n const stats = useMemo(() => {\n const activeCoupons = coupons.filter((c: Coupon) => c.status === 'ACTIVE').length;\n const totalUses = coupons.reduce((sum: number, c: Coupon) => sum + c.currentUses, 0);\n const totalDiscountValue = coupons.reduce((sum: number, c: Coupon) => {\n if (c.type === 'FIXED_AMOUNT') return sum + c.value * c.currentUses;\n return sum;\n }, 0);\n return { totalCoupons: total, activeCoupons, totalUses, totalDiscountValue };\n }, [coupons, total]);\n\n // Check permissions\n if (!permissions.canManageDiscounts) {\n return <AccessDenied message=\"You don't have permission to manage coupons.\" />;\n }\n\n // Format currency\n const formatCurrency = (amount: number) => {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n }).format(amount);\n };\n\n // Format date\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n };\n\n // Get status color\n const getStatusColor = (status: string) => {\n switch (status) {\n case 'ACTIVE':\n return 'text-status-success-text bg-status-success-bg-subtle';\n case 'INACTIVE':\n return 'text-text-secondary bg-bg-canvas';\n case 'EXPIRED':\n return 'text-status-error-text bg-status-error-bg-subtle';\n default:\n return 'text-text-secondary bg-bg-canvas';\n }\n };\n\n // Get type icon\n const getTypeIcon = (type: string) => {\n switch (type) {\n case 'PERCENTAGE':\n return <Percent className=\"size-4\" />;\n case 'FIXED_AMOUNT':\n return <DollarSign className=\"size-4\" />;\n case 'FREE_SHIPPING':\n return <Package className=\"size-4\" />;\n default:\n return <Tag className=\"size-4\" />;\n }\n };\n\n const couponColumns: ResponsiveTableColumn<Coupon>[] = [\n {\n key: 'code',\n header: 'Code',\n priority: 'primary',\n cell: (coupon) => (\n <div>\n <p className=\"font-medium text-text-primary font-mono\">{coupon.code}</p>\n <p className=\"text-sm text-text-muted\">{coupon.name}</p>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n cell: (coupon) => (\n <div className=\"flex items-center gap-2\">\n {getTypeIcon(coupon.type)}\n <span className=\"text-sm text-text-primary capitalize\">\n {coupon.type.replace('_', ' ').toLowerCase()}\n </span>\n </div>\n ),\n },\n {\n key: 'value',\n header: 'Value',\n cell: (coupon) => (\n <span className=\"text-sm font-medium text-text-primary tabular-nums\">\n {coupon.type === 'PERCENTAGE' ? `${coupon.value}%` : formatCurrency(coupon.value)}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n cell: (coupon) => (\n <span\n className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(\n coupon.status\n )}`}\n >\n {coupon.status}\n </span>\n ),\n },\n {\n key: 'usage',\n header: 'Usage',\n cell: (coupon) => (\n <>\n <div className=\"text-sm text-text-primary tabular-nums\">\n <span className=\"font-medium\">{coupon.currentUses}</span>\n {coupon.maxTotalUses && (\n <span className=\"text-text-muted\"> / {coupon.maxTotalUses}</span>\n )}\n </div>\n {coupon.maxTotalUses && (\n <div className=\"w-full bg-bg-sunken rounded-full h-1.5 mt-1\">\n <div\n className=\"bg-action-primary-bg h-1.5 rounded-full\"\n style={{\n width: `${Math.min((coupon.currentUses / coupon.maxTotalUses) * 100, 100)}%`,\n }}\n />\n </div>\n )}\n </>\n ),\n },\n {\n key: 'validUntil',\n header: 'Valid Until',\n priority: 'hidden-on-mobile',\n cell: (coupon) => (\n <span className=\"text-sm text-text-muted\">\n {coupon.validUntil ? formatDate(coupon.validUntil) : 'No expiry'}\n </span>\n ),\n },\n ];\n\n const renderCouponActions = (coupon: Coupon) => (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/coupons/${coupon.id}`)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken text-text-muted hover:text-text-primary\"\n title=\"View details\"\n >\n <Eye className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/coupons/${coupon.id}/edit`)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken text-text-muted hover:text-text-primary\"\n title=\"Edit coupon\"\n >\n <Edit className=\"size-4\" />\n </button>\n {coupon.status === 'ACTIVE' ? (\n <button\n type=\"button\"\n onClick={() => setCouponToDeactivate(coupon)}\n disabled={deactivating || togglingId === coupon.id}\n className=\"p-2 rounded-lg hover:bg-status-error-bg-subtle text-text-muted hover:text-status-error-text disabled:opacity-50 disabled:cursor-not-allowed\"\n title=\"Deactivate coupon\"\n >\n <PowerOff className=\"size-4\" />\n </button>\n ) : coupon.status === 'INACTIVE' ? (\n <button\n type=\"button\"\n onClick={() => handleToggleStatus(coupon.id, CouponStatus.Active)}\n disabled={togglingId === coupon.id}\n className=\"p-2 rounded-lg hover:bg-status-success-bg-subtle text-text-muted hover:text-status-success-text disabled:opacity-50 disabled:cursor-not-allowed\"\n title=\"Activate coupon\"\n >\n <Power className=\"size-4\" />\n </button>\n ) : (\n <button\n type=\"button\"\n disabled\n className=\"p-2 rounded-lg text-text-muted/40 cursor-not-allowed\"\n title={`Cannot change status (${coupon.status.toLowerCase()})`}\n >\n <Trash2 className=\"size-4\" />\n </button>\n )}\n </>\n );\n\n return (\n <div className=\"max-w-7xl mx-auto p-4 sm:p-6 space-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <PageHeader\n title={tr('billing.coupons.title', 'Coupons')}\n description=\"Manage discount codes and promotional offers\"\n icon={<Tag className=\"size-6\" />}\n />\n\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons/create')}\n className=\"inline-flex items-center gap-2 rounded-button bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n Create Coupon\n </button>\n </div>\n\n {/* Statistics Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <div className=\"rounded-card border border-border-subtle bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Coupons</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Tag className=\"size-4 text-accent-blue\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {stats.totalCoupons}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">{stats.activeCoupons} active</p>\n </div>\n\n <div className=\"rounded-card border border-border-subtle bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Uses</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-status-success-bg-subtle\">\n <TrendingUp className=\"size-4 text-status-success-text\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">{stats.totalUses}</p>\n <p className=\"text-xs text-text-muted mt-1\">All time</p>\n </div>\n\n <div className=\"rounded-card border border-border-subtle bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Discount</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <DollarSign className=\"size-4 text-accent-purple\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {formatCurrency(stats.totalDiscountValue)}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">Fixed amount only</p>\n </div>\n\n <div className=\"rounded-card border border-border-subtle bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Active Rate</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-status-success-bg-subtle\">\n <CheckCircle className=\"size-4 text-status-success-text\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {stats.totalCoupons > 0\n ? Math.round((stats.activeCoupons / stats.totalCoupons) * 100)\n : 0}\n %\n </p>\n <p className=\"text-xs text-text-muted mt-1\">Of all coupons</p>\n </div>\n </div>\n\n {feedback && (\n <div\n className={\n feedback.tone === 'success'\n ? 'flex items-start gap-3 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-4 text-status-success-text'\n : 'flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text'\n }\n role=\"status\"\n aria-live=\"polite\"\n >\n {feedback.tone === 'success' ? (\n <CheckCircle className=\"mt-0.5 size-4 shrink-0\" />\n ) : (\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n )}\n <p className=\"text-sm font-medium\">{feedback.message}</p>\n </div>\n )}\n\n {couponToDeactivate && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"font-medium text-status-error-text\">\n Deactivate coupon <span className=\"font-mono\">{couponToDeactivate.code}</span>?\n </p>\n <p className=\"mt-1 text-sm text-status-error-text\">\n This will prevent new checkouts from applying this coupon.\n </p>\n </div>\n <div className=\"flex gap-2 self-end sm:self-auto\">\n <button\n type=\"button\"\n onClick={() => setCouponToDeactivate(null)}\n className=\"rounded-button border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => handleDeactivate(couponToDeactivate.id)}\n disabled={deactivating}\n className=\"rounded-button bg-action-danger-bg px-3 py-2 text-sm font-medium text-action-danger-text transition-colors hover:bg-action-danger-bgHover disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {deactivating ? 'Deactivating…' : 'Deactivate'}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-4\">\n {/* Search */}\n <div className=\"md:col-span-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted\" />\n <input\n type=\"text\"\n placeholder=\"Search coupons...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as CouponStatusFilter);\n setPage(0);\n }}\n className=\"px-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"ALL\">All Status</option>\n <option value=\"ACTIVE\">Active</option>\n <option value=\"INACTIVE\">Inactive</option>\n <option value=\"EXPIRED\">Expired</option>\n </select>\n\n {/* Type Filter */}\n <select\n value={typeFilter}\n onChange={(e) => {\n setTypeFilter(e.target.value as CouponTypeFilter);\n setPage(0);\n }}\n className=\"px-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"ALL\">All Types</option>\n <option value=\"PERCENTAGE\">Percentage</option>\n <option value=\"FIXED_AMOUNT\">Fixed Amount</option>\n <option value=\"FREE_SHIPPING\">Free Shipping</option>\n </select>\n </div>\n </div>\n\n {/* Coupons Table */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-card overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n {loading ? (\n <div className=\"p-8 text-center text-text-muted\">Loading coupons…</div>\n ) : (\n <ResponsiveTable<Coupon>\n columns={couponColumns}\n data={filteredCoupons}\n rowKey={(coupon) => coupon.id}\n rowActions={renderCouponActions}\n emptyState={<div className=\"p-8 text-center text-text-muted\">No coupons found</div>}\n />\n )}\n </div>\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <div className=\"flex items-center justify-between px-2\">\n <p className=\"text-sm text-text-muted\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}{' '}\n coupons\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n disabled={page === 0}\n className=\"p-2 rounded-lg border border-border-subtle hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-primary font-medium\">\n {page + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setPage((p) => p + 1)}\n disabled={!hasMore}\n className=\"p-2 rounded-lg border border-border-subtle hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n\n {/* Export Button */}\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"px-4 py-2 rounded-lg border border-border-subtle text-text-primary hover:bg-bg-sunken flex items-center gap-2\"\n >\n <Download className=\"size-4\" />\n Export Coupons\n </button>\n </div>\n </div>\n );\n\n async function handleDeactivate(couponId: string) {\n try {\n await deactivateCoupon({ variables: { id: couponId } });\n await refetch();\n setCouponToDeactivate(null);\n setFeedback({ tone: 'success', message: 'Coupon deactivated successfully.' });\n } catch (error) {\n console.error('Failed to deactivate coupon:', error);\n setFeedback({\n tone: 'error',\n message: `Failed to deactivate coupon: ${error instanceof Error ? error.message : 'Unknown error'}`,\n });\n }\n }\n\n async function handleToggleStatus(couponId: string, newStatus: CouponStatus) {\n setTogglingId(couponId);\n try {\n await updateCoupon({ variables: { id: couponId, input: { status: newStatus } } });\n await refetch();\n setFeedback({\n tone: 'success',\n message: `Coupon ${newStatus === CouponStatus.Active ? 'activated' : 'deactivated'} successfully.`,\n });\n } catch (error) {\n console.error('Failed to update coupon status:', error);\n setFeedback({\n tone: 'error',\n message: `Failed to update status: ${error instanceof Error ? error.message : 'Unknown error'}`,\n });\n } finally {\n setTogglingId(null);\n }\n }\n\n function handleExport() {\n const rows: CouponExportRow[] = filteredCoupons.map((coupon: Coupon) => ({\n code: coupon.code,\n name: coupon.name,\n description: coupon.description ?? '',\n status: coupon.status,\n type: coupon.type,\n value: coupon.value,\n currentUses: coupon.currentUses,\n maxTotalUses: coupon.maxTotalUses ?? '',\n validUntil: coupon.validUntil ?? '',\n }));\n\n if (rows.length === 0) {\n setFeedback({\n tone: 'error',\n message: 'There are no coupons to export for the current filters.',\n });\n return;\n }\n\n const csvHeader = Object.keys(rows[0]).join(',');\n const csvBody = rows\n .map((row: CouponExportRow) =>\n Object.values(row)\n .map((value) => `\"${String(value).replaceAll('\"', '\"\"')}\"`)\n .join(',')\n )\n .join('\\n');\n\n const blob = new Blob([`${csvHeader}\\n${csvBody}`], { type: 'text/csv;charset=utf-8' });\n const downloadUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = downloadUrl;\n link.download = 'billing-coupons.csv';\n link.click();\n URL.revokeObjectURL(downloadUrl);\n\n setFeedback({\n tone: 'success',\n message: `Exported ${rows.length} coupon${rows.length === 1 ? '' : 's'} to CSV.`,\n });\n }\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CouponsListPage.js","names":[],"sources":["../../../../../src/billing/modules/coupons/pages/CouponsListPage.tsx"],"sourcesContent":["/**\n * Coupons List Page\n *\n * Admin interface for managing coupon codes:\n * - View all coupons\n * - Filter by status, type, scope\n * - Create new coupons\n * - Edit/deactivate existing coupons\n * - View usage analytics\n */\n\nimport { type FC, useState, useMemo, useEffect } from 'react';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport {\n Plus,\n Search,\n Tag,\n TrendingUp,\n CheckCircle,\n AlertCircle,\n Percent,\n DollarSign,\n Package,\n Eye,\n Edit,\n Trash2,\n Download,\n PowerOff,\n Power,\n ChevronLeft,\n ChevronRight,\n} from 'lucide-react';\n\nimport { ResponsiveTable, type ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport {\n useListCouponsQuery,\n useDeactivateCouponMutation,\n useUpdateCouponMutation,\n} from '../../../../generated/global-operations';\nimport { CouponStatus } from '../../../../generated/global-types';\nimport type {\n CouponScope as GQLCouponScope,\n CouponStatus as GQLCouponStatus,\n CouponType as GQLCouponType,\n Coupon as GQLCoupon,\n} from '../../../../generated/global-types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype Coupon = GQLCoupon;\n\ntype CouponStatusFilter = 'ALL' | GQLCouponStatus;\ntype CouponTypeFilter = 'ALL' | GQLCouponType;\ntype CouponScopeFilter = 'ALL' | GQLCouponScope;\n\ninterface CouponExportRow {\n code: string;\n name: string;\n description: string;\n status: string;\n type: string;\n value: number;\n currentUses: number;\n maxTotalUses: number | '';\n validUntil: string;\n}\n\nexport const CouponsListPage: 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 PAGE_SIZE = 20;\n\n // State\n const [searchQuery, setSearchQuery] = useState('');\n const [statusFilter, setStatusFilter] = useState<CouponStatusFilter>('ALL');\n const [typeFilter, setTypeFilter] = useState<CouponTypeFilter>('ALL');\n const [scopeFilter] = useState<CouponScopeFilter>('ALL');\n const [page, setPage] = useState(0);\n const [couponToDeactivate, setCouponToDeactivate] = useState<Coupon | null>(null);\n const [togglingId, setTogglingId] = useState<string | null>(null);\n const [feedback, setFeedback] = useState<{\n tone: 'success' | 'error';\n message: string;\n } | null>(null);\n\n // Fetch coupons from backend (server-side pagination)\n const { data, loading, refetch } = useListCouponsQuery({\n variables: {\n limit: PAGE_SIZE,\n offset: page * PAGE_SIZE,\n filter: {\n status: statusFilter !== 'ALL' ? (statusFilter as GQLCouponStatus) : undefined,\n type: typeFilter !== 'ALL' ? (typeFilter as GQLCouponType) : undefined,\n scope: scopeFilter !== 'ALL' ? (scopeFilter as GQLCouponScope) : undefined,\n search: searchQuery || undefined,\n },\n },\n });\n\n const coupons = useMemo<Coupon[]>(\n () => data?.listCoupons?.coupons ?? [],\n [data?.listCoupons?.coupons]\n );\n const total = data?.listCoupons?.total ?? 0;\n const hasMore = data?.listCoupons?.hasMore ?? false;\n const totalPages = Math.ceil(total / PAGE_SIZE);\n\n // Mutations\n const [deactivateCoupon, { loading: deactivating }] = useDeactivateCouponMutation();\n const [updateCoupon] = useUpdateCouponMutation();\n\n // With server-side filtering, filteredCoupons === coupons\n const filteredCoupons = coupons;\n\n // Reset to first page when filters change\n useEffect(() => {\n setPage(0);\n }, [searchQuery, statusFilter, typeFilter, scopeFilter]);\n\n // Statistics from current page (best effort without a separate aggregation query)\n const stats = useMemo(() => {\n const activeCoupons = coupons.filter((c: Coupon) => c.status === 'ACTIVE').length;\n const totalUses = coupons.reduce((sum: number, c: Coupon) => sum + c.currentUses, 0);\n const totalDiscountValue = coupons.reduce((sum: number, c: Coupon) => {\n if (c.type === 'FIXED_AMOUNT') return sum + c.value * c.currentUses;\n return sum;\n }, 0);\n return { totalCoupons: total, activeCoupons, totalUses, totalDiscountValue };\n }, [coupons, total]);\n\n // Check permissions\n if (!permissions.canManageDiscounts) {\n return <AccessDenied message=\"You don't have permission to manage coupons.\" />;\n }\n\n // Format currency\n const formatCurrency = (amount: number) => {\n return new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency: 'USD',\n }).format(amount);\n };\n\n // Format date\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n });\n };\n\n // Get status color\n const getStatusColor = (status: string) => {\n switch (status) {\n case 'ACTIVE':\n return 'text-status-success-text bg-status-success-bg-subtle';\n case 'INACTIVE':\n return 'text-text-secondary bg-bg-canvas';\n case 'EXPIRED':\n return 'text-status-error-text bg-status-error-bg-subtle';\n default:\n return 'text-text-secondary bg-bg-canvas';\n }\n };\n\n // Get type icon\n const getTypeIcon = (type: string) => {\n switch (type) {\n case 'PERCENTAGE':\n return <Percent className=\"size-4\" />;\n case 'FIXED_AMOUNT':\n return <DollarSign className=\"size-4\" />;\n case 'FREE_SHIPPING':\n return <Package className=\"size-4\" />;\n default:\n return <Tag className=\"size-4\" />;\n }\n };\n\n const couponColumns: ResponsiveTableColumn<Coupon>[] = [\n {\n key: 'code',\n header: 'Code',\n priority: 'primary',\n cell: (coupon) => (\n <div>\n <p className=\"font-medium text-text-primary font-mono\">{coupon.code}</p>\n <p className=\"text-sm text-text-muted\">{coupon.name}</p>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n cell: (coupon) => (\n <div className=\"flex items-center gap-2\">\n {getTypeIcon(coupon.type)}\n <span className=\"text-sm text-text-primary capitalize\">\n {coupon.type.replace('_', ' ').toLowerCase()}\n </span>\n </div>\n ),\n },\n {\n key: 'value',\n header: 'Value',\n cell: (coupon) => (\n <span className=\"text-sm font-medium text-text-primary tabular-nums\">\n {coupon.type === 'PERCENTAGE' ? `${coupon.value}%` : formatCurrency(coupon.value)}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n cell: (coupon) => (\n <span\n className={`inline-flex px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(\n coupon.status\n )}`}\n >\n {coupon.status}\n </span>\n ),\n },\n {\n key: 'usage',\n header: 'Usage',\n cell: (coupon) => (\n <>\n <div className=\"text-sm text-text-primary tabular-nums\">\n <span className=\"font-medium\">{coupon.currentUses}</span>\n {coupon.maxTotalUses && (\n <span className=\"text-text-muted\"> / {coupon.maxTotalUses}</span>\n )}\n </div>\n {coupon.maxTotalUses && (\n <div className=\"w-full bg-bg-sunken rounded-full h-1.5 mt-1\">\n <div\n className=\"bg-action-primary-bg h-1.5 rounded-full\"\n style={{\n width: `${Math.min((coupon.currentUses / coupon.maxTotalUses) * 100, 100)}%`,\n }}\n />\n </div>\n )}\n </>\n ),\n },\n {\n key: 'validUntil',\n header: 'Valid Until',\n priority: 'hidden-on-mobile',\n cell: (coupon) => (\n <span className=\"text-sm text-text-muted\">\n {coupon.validUntil ? formatDate(coupon.validUntil) : 'No expiry'}\n </span>\n ),\n },\n ];\n\n const renderCouponActions = (coupon: Coupon) => (\n <>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/coupons/${coupon.id}`)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken text-text-muted hover:text-text-primary\"\n title=\"View details\"\n >\n <Eye className=\"size-4\" />\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/coupons/${coupon.id}/edit`)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken text-text-muted hover:text-text-primary\"\n title=\"Edit coupon\"\n >\n <Edit className=\"size-4\" />\n </button>\n {coupon.status === 'ACTIVE' ? (\n <button\n type=\"button\"\n onClick={() => setCouponToDeactivate(coupon)}\n disabled={deactivating || togglingId === coupon.id}\n className=\"p-2 rounded-lg hover:bg-status-error-bg-subtle text-text-muted hover:text-status-error-text disabled:opacity-50 disabled:cursor-not-allowed\"\n title=\"Deactivate coupon\"\n >\n <PowerOff className=\"size-4\" />\n </button>\n ) : coupon.status === 'INACTIVE' ? (\n <button\n type=\"button\"\n onClick={() => handleToggleStatus(coupon.id, CouponStatus.Active)}\n disabled={togglingId === coupon.id}\n className=\"p-2 rounded-lg hover:bg-status-success-bg-subtle text-text-muted hover:text-status-success-text disabled:opacity-50 disabled:cursor-not-allowed\"\n title=\"Activate coupon\"\n >\n <Power className=\"size-4\" />\n </button>\n ) : (\n <button\n type=\"button\"\n disabled\n className=\"p-2 rounded-lg text-text-muted/40 cursor-not-allowed\"\n title={`Cannot change status (${coupon.status.toLowerCase()})`}\n >\n <Trash2 className=\"size-4\" />\n </button>\n )}\n </>\n );\n\n return (\n <div className=\"max-w-7xl mx-auto p-4 sm:p-6 space-y-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between\">\n <PageHeader\n title={tr('billing.coupons.title', 'Coupons')}\n description=\"Manage discount codes and promotional offers\"\n icon={<Tag className=\"size-6\" />}\n />\n\n <button\n type=\"button\"\n onClick={() => navigateTo('/coupons/create')}\n className=\"inline-flex items-center gap-2 rounded-button bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors duration-200 hover:bg-action-primary-bgHover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Plus className=\"size-4\" />\n Create Coupon\n </button>\n </div>\n\n {/* Statistics Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4\">\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Coupons</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Tag className=\"size-4 text-accent-blue\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {stats.totalCoupons}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">{stats.activeCoupons} active</p>\n </div>\n\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Uses</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-status-success-bg-subtle\">\n <TrendingUp className=\"size-4 text-status-success-text\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">{stats.totalUses}</p>\n <p className=\"text-xs text-text-muted mt-1\">All time</p>\n </div>\n\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Total Discount</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <DollarSign className=\"size-4 text-accent-purple\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {formatCurrency(stats.totalDiscountValue)}\n </p>\n <p className=\"text-xs text-text-muted mt-1\">Fixed amount only</p>\n </div>\n\n <div className=\"rounded-card border border-border-seam bg-bg-surface p-5 shadow-[var(--shadow-elevation-1)]\">\n <div className=\"flex items-center justify-between mb-2\">\n <span className=\"text-sm text-text-secondary\">Active Rate</span>\n <span className=\"flex size-9 items-center justify-center rounded-lg bg-status-success-bg-subtle\">\n <CheckCircle className=\"size-4 text-status-success-text\" />\n </span>\n </div>\n <p className=\"text-2xl font-semibold text-text-primary tabular-nums\">\n {stats.totalCoupons > 0\n ? Math.round((stats.activeCoupons / stats.totalCoupons) * 100)\n : 0}\n %\n </p>\n <p className=\"text-xs text-text-muted mt-1\">Of all coupons</p>\n </div>\n </div>\n\n {feedback && (\n <div\n className={\n feedback.tone === 'success'\n ? 'flex items-start gap-3 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-4 text-status-success-text'\n : 'flex items-start gap-3 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-status-error-text'\n }\n role=\"status\"\n aria-live=\"polite\"\n >\n {feedback.tone === 'success' ? (\n <CheckCircle className=\"mt-0.5 size-4 shrink-0\" />\n ) : (\n <AlertCircle className=\"mt-0.5 size-4 shrink-0\" />\n )}\n <p className=\"text-sm font-medium\">{feedback.message}</p>\n </div>\n )}\n\n {couponToDeactivate && (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"font-medium text-status-error-text\">\n Deactivate coupon <span className=\"font-mono\">{couponToDeactivate.code}</span>?\n </p>\n <p className=\"mt-1 text-sm text-status-error-text\">\n This will prevent new checkouts from applying this coupon.\n </p>\n </div>\n <div className=\"flex gap-2 self-end sm:self-auto\">\n <button\n type=\"button\"\n onClick={() => setCouponToDeactivate(null)}\n className=\"rounded-button border border-border-subtle bg-bg-surface px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => handleDeactivate(couponToDeactivate.id)}\n disabled={deactivating}\n className=\"rounded-button bg-action-danger-bg px-3 py-2 text-sm font-medium text-action-danger-text transition-colors hover:bg-action-danger-bgHover disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {deactivating ? 'Deactivating…' : 'Deactivate'}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"bg-bg-surface border border-border-subtle rounded-lg p-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-4\">\n {/* Search */}\n <div className=\"md:col-span-2\">\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted\" />\n <input\n type=\"text\"\n placeholder=\"Search coupons...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full pl-10 pr-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n </div>\n\n {/* Status Filter */}\n <select\n value={statusFilter}\n onChange={(e) => {\n setStatusFilter(e.target.value as CouponStatusFilter);\n setPage(0);\n }}\n className=\"px-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"ALL\">All Status</option>\n <option value=\"ACTIVE\">Active</option>\n <option value=\"INACTIVE\">Inactive</option>\n <option value=\"EXPIRED\">Expired</option>\n </select>\n\n {/* Type Filter */}\n <select\n value={typeFilter}\n onChange={(e) => {\n setTypeFilter(e.target.value as CouponTypeFilter);\n setPage(0);\n }}\n className=\"px-4 py-2 rounded-input border border-border-subtle bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"ALL\">All Types</option>\n <option value=\"PERCENTAGE\">Percentage</option>\n <option value=\"FIXED_AMOUNT\">Fixed Amount</option>\n <option value=\"FREE_SHIPPING\">Free Shipping</option>\n </select>\n </div>\n </div>\n\n {/* Coupons Table */}\n <div className=\"bg-bg-surface border border-border-seam rounded-card overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n {loading ? (\n <div className=\"p-8 text-center text-text-muted\">Loading coupons…</div>\n ) : (\n <ResponsiveTable<Coupon>\n columns={couponColumns}\n data={filteredCoupons}\n rowKey={(coupon) => coupon.id}\n rowActions={renderCouponActions}\n emptyState={<div className=\"p-8 text-center text-text-muted\">No coupons found</div>}\n />\n )}\n </div>\n\n {/* Pagination */}\n {total > PAGE_SIZE && (\n <div className=\"flex items-center justify-between px-2\">\n <p className=\"text-sm text-text-muted\">\n Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}{' '}\n coupons\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n disabled={page === 0}\n className=\"p-2 rounded-lg border border-border-subtle hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronLeft className=\"size-4\" />\n </button>\n <span className=\"text-sm text-text-primary font-medium\">\n {page + 1} / {totalPages}\n </span>\n <button\n type=\"button\"\n onClick={() => setPage((p) => p + 1)}\n disabled={!hasMore}\n className=\"p-2 rounded-lg border border-border-subtle hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed\"\n >\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n\n {/* Export Button */}\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"px-4 py-2 rounded-lg border border-border-subtle text-text-primary hover:bg-bg-sunken flex items-center gap-2\"\n >\n <Download className=\"size-4\" />\n Export Coupons\n </button>\n </div>\n </div>\n );\n\n async function handleDeactivate(couponId: string) {\n try {\n await deactivateCoupon({ variables: { id: couponId } });\n await refetch();\n setCouponToDeactivate(null);\n setFeedback({ tone: 'success', message: 'Coupon deactivated successfully.' });\n } catch (error) {\n console.error('Failed to deactivate coupon:', error);\n setFeedback({\n tone: 'error',\n message: `Failed to deactivate coupon: ${error instanceof Error ? error.message : 'Unknown error'}`,\n });\n }\n }\n\n async function handleToggleStatus(couponId: string, newStatus: CouponStatus) {\n setTogglingId(couponId);\n try {\n await updateCoupon({ variables: { id: couponId, input: { status: newStatus } } });\n await refetch();\n setFeedback({\n tone: 'success',\n message: `Coupon ${newStatus === CouponStatus.Active ? 'activated' : 'deactivated'} successfully.`,\n });\n } catch (error) {\n console.error('Failed to update coupon status:', error);\n setFeedback({\n tone: 'error',\n message: `Failed to update status: ${error instanceof Error ? error.message : 'Unknown error'}`,\n });\n } finally {\n setTogglingId(null);\n }\n }\n\n function handleExport() {\n const rows: CouponExportRow[] = filteredCoupons.map((coupon: Coupon) => ({\n code: coupon.code,\n name: coupon.name,\n description: coupon.description ?? '',\n status: coupon.status,\n type: coupon.type,\n value: coupon.value,\n currentUses: coupon.currentUses,\n maxTotalUses: coupon.maxTotalUses ?? '',\n validUntil: coupon.validUntil ?? '',\n }));\n\n if (rows.length === 0) {\n setFeedback({\n tone: 'error',\n message: 'There are no coupons to export for the current filters.',\n });\n return;\n }\n\n const csvHeader = Object.keys(rows[0]).join(',');\n const csvBody = rows\n .map((row: CouponExportRow) =>\n Object.values(row)\n .map((value) => `\"${String(value).replaceAll('\"', '\"\"')}\"`)\n .join(',')\n )\n .join('\\n');\n\n const blob = new Blob([`${csvHeader}\\n${csvBody}`], { type: 'text/csv;charset=utf-8' });\n const downloadUrl = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = downloadUrl;\n link.download = 'billing-coupons.csv';\n link.click();\n URL.revokeObjectURL(downloadUrl);\n\n setFeedback({\n tone: 'success',\n message: `Exported ${rows.length} coupon${rows.length === 1 ? '' : 's'} to CSV.`,\n });\n }\n};\n"],"mappings":"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"}
|
|
@@ -95,7 +95,7 @@ var u = () => {
|
|
|
95
95
|
})
|
|
96
96
|
]
|
|
97
97
|
}), /* @__PURE__ */ l("div", {
|
|
98
|
-
className: "border border-border-
|
|
98
|
+
className: "border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]",
|
|
99
99
|
children: [/* @__PURE__ */ c("div", {
|
|
100
100
|
className: "p-4 border-b border-border-subtle bg-bg-sunken/30",
|
|
101
101
|
children: /* @__PURE__ */ c("h2", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminCreditsPage.js","names":[],"sources":["../../../../../src/billing/modules/credits/pages/AdminCreditsPage.tsx"],"sourcesContent":["import { useMemo, useState, type FC } from 'react';\n\nimport { useGetBillingAccountsSettingsQuery } from '../../../../generated/global-operations';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport type { BillingAccount } from '../../../shared/types';\nimport { useCreditMutations, useCreditTransactions } from '../hooks/useCredits';\n\nexport const AdminCreditsPage: FC = () => {\n const permissions = useBillingPermissions();\n const { grantCredits } = useCreditMutations();\n const { data, loading, refetch } = useGetBillingAccountsSettingsQuery();\n const billingAccounts = useMemo<BillingAccount[]>(\n () => data?.getBillingAccountsByOrg || [],\n [data]\n );\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string>('');\n const [amount, setAmount] = useState<string>('');\n const [reason, setReason] = useState<string>('');\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [message, setMessage] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const activeBillingAccountId = selectedBillingAccountId || billingAccounts[0]?.id || '';\n const {\n transactions,\n isLoading: isLoadingTransactions,\n refetch: refetchTransactions,\n } = useCreditTransactions({\n billingAccountId: activeBillingAccountId,\n days: 30,\n page: 1,\n pageSize: 10,\n });\n\n if (!permissions.canGrantCredits) {\n return <AccessDenied message=\"You don't have permission to grant credits.\" />;\n }\n\n const handleGrantCredits = async () => {\n if (!activeBillingAccountId) {\n setError('Select a billing account first.');\n return;\n }\n\n if (!amount || Number(amount) <= 0) {\n setError('Enter a valid credit amount.');\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n setMessage(null);\n\n try {\n await grantCredits({\n billingAccountId: activeBillingAccountId,\n amount: Number(amount),\n reason,\n });\n\n await Promise.all([refetch(), refetchTransactions()]);\n setAmount('');\n setReason('');\n setMessage('Credits granted successfully.');\n } catch (grantError) {\n setError(grantError instanceof Error ? grantError.message : 'Failed to grant credits.');\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title=\"Credit Grants\"\n description=\"Grant credits to billing accounts and review the latest credit ledger activity.\"\n />\n\n <div className=\"grid grid-cols-1 xl:grid-cols-[360px_minmax(0,1fr)] gap-6\">\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface p-6 space-y-4 h-fit\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Billing Account\n </label>\n <select\n value={activeBillingAccountId}\n onChange={(event) => setSelectedBillingAccountId(event.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {billingAccounts.map((account) => (\n <option key={account.id} value={account.id}>\n {account.name}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Credits</label>\n <input\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={amount}\n onChange={(event) => setAmount(event.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"500\"\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Reason</label>\n <textarea\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n className=\"w-full min-h-24 px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"Promotional credit grant, service recovery, manual adjustment...\"\n />\n </div>\n\n {message ? <p className=\"text-sm text-status-success-text\">{message}</p> : null}\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n\n <button\n type=\"button\"\n onClick={handleGrantCredits}\n disabled={loading || isSubmitting || billingAccounts.length === 0}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isSubmitting ? 'Granting Credits...' : 'Grant Credits'}\n </button>\n </div>\n\n <div className=\"border border-border-
|
|
1
|
+
{"version":3,"file":"AdminCreditsPage.js","names":[],"sources":["../../../../../src/billing/modules/credits/pages/AdminCreditsPage.tsx"],"sourcesContent":["import { useMemo, useState, type FC } from 'react';\n\nimport { useGetBillingAccountsSettingsQuery } from '../../../../generated/global-operations';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport type { BillingAccount } from '../../../shared/types';\nimport { useCreditMutations, useCreditTransactions } from '../hooks/useCredits';\n\nexport const AdminCreditsPage: FC = () => {\n const permissions = useBillingPermissions();\n const { grantCredits } = useCreditMutations();\n const { data, loading, refetch } = useGetBillingAccountsSettingsQuery();\n const billingAccounts = useMemo<BillingAccount[]>(\n () => data?.getBillingAccountsByOrg || [],\n [data]\n );\n const [selectedBillingAccountId, setSelectedBillingAccountId] = useState<string>('');\n const [amount, setAmount] = useState<string>('');\n const [reason, setReason] = useState<string>('');\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [message, setMessage] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const activeBillingAccountId = selectedBillingAccountId || billingAccounts[0]?.id || '';\n const {\n transactions,\n isLoading: isLoadingTransactions,\n refetch: refetchTransactions,\n } = useCreditTransactions({\n billingAccountId: activeBillingAccountId,\n days: 30,\n page: 1,\n pageSize: 10,\n });\n\n if (!permissions.canGrantCredits) {\n return <AccessDenied message=\"You don't have permission to grant credits.\" />;\n }\n\n const handleGrantCredits = async () => {\n if (!activeBillingAccountId) {\n setError('Select a billing account first.');\n return;\n }\n\n if (!amount || Number(amount) <= 0) {\n setError('Enter a valid credit amount.');\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n setMessage(null);\n\n try {\n await grantCredits({\n billingAccountId: activeBillingAccountId,\n amount: Number(amount),\n reason,\n });\n\n await Promise.all([refetch(), refetchTransactions()]);\n setAmount('');\n setReason('');\n setMessage('Credits granted successfully.');\n } catch (grantError) {\n setError(grantError instanceof Error ? grantError.message : 'Failed to grant credits.');\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title=\"Credit Grants\"\n description=\"Grant credits to billing accounts and review the latest credit ledger activity.\"\n />\n\n <div className=\"grid grid-cols-1 xl:grid-cols-[360px_minmax(0,1fr)] gap-6\">\n <div className=\"border border-border-subtle rounded-lg bg-bg-surface p-6 space-y-4 h-fit\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n Billing Account\n </label>\n <select\n value={activeBillingAccountId}\n onChange={(event) => setSelectedBillingAccountId(event.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {billingAccounts.map((account) => (\n <option key={account.id} value={account.id}>\n {account.name}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Credits</label>\n <input\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={amount}\n onChange={(event) => setAmount(event.target.value)}\n className=\"w-full px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"500\"\n />\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">Reason</label>\n <textarea\n value={reason}\n onChange={(event) => setReason(event.target.value)}\n className=\"w-full min-h-24 px-3 py-2 border border-border-subtle rounded-input bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n placeholder=\"Promotional credit grant, service recovery, manual adjustment...\"\n />\n </div>\n\n {message ? <p className=\"text-sm text-status-success-text\">{message}</p> : null}\n {error ? <p className=\"text-sm text-status-error-text\">{error}</p> : null}\n\n <button\n type=\"button\"\n onClick={handleGrantCredits}\n disabled={loading || isSubmitting || billingAccounts.length === 0}\n className=\"w-full px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isSubmitting ? 'Granting Credits...' : 'Grant Credits'}\n </button>\n </div>\n\n <div className=\"border border-border-seam rounded-card bg-bg-surface overflow-hidden shadow-[var(--shadow-elevation-1)]\">\n <div className=\"p-4 border-b border-border-subtle bg-bg-sunken/30\">\n <h2 className=\"font-semibold text-text-primary\">Recent Credit Ledger</h2>\n </div>\n\n {isLoadingTransactions ? (\n <div className=\"p-6 text-sm text-text-muted\">Loading recent credit activity…</div>\n ) : transactions.length === 0 ? (\n <div className=\"p-6 text-sm text-text-muted\">No recent credit transactions found.</div>\n ) : (\n <div className=\"divide-y divide-border-subtle\">\n {transactions.map((transaction) => (\n <div key={transaction.id} className=\"p-4 flex items-center justify-between gap-4\">\n <div>\n <p className=\"text-sm font-medium text-text-primary\">{transaction.type}</p>\n <p className=\"text-xs text-text-muted\">\n {(transaction.context as { reason?: string } | undefined)?.reason ||\n 'No reason'}\n </p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {transaction.amount > 0 ? '+' : ''}\n {transaction.amount}\n </p>\n <p className=\"text-xs text-text-muted\">{transaction.status}</p>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAQA,IAAa,UAA6B;CACxC,IAAM,IAAc,GAAuB,EACrC,EAAE,oBAAiB,GAAoB,EACvC,EAAE,SAAM,YAAS,eAAY,GAAoC,EACjE,IAAkB,QAChB,GAAM,2BAA2B,EAAE,EACzC,CAAC,EAAK,CACP,EACK,CAAC,GAA0B,KAA+B,EAAiB,GAAG,EAC9E,CAAC,GAAQ,KAAa,EAAiB,GAAG,EAC1C,CAAC,GAAQ,KAAa,EAAiB,GAAG,EAC1C,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAS,KAAc,EAAwB,KAAK,EACrD,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAyB,KAA4B,EAAgB,IAAI,MAAM,IAC/E,EACJ,iBACA,WAAW,GACX,SAAS,MACP,EAAsB;EACxB,kBAAkB;EAClB,MAAM;EACN,MAAM;EACN,UAAU;EACX,CAAC;AAuCF,QArCK,EAAY,kBAsCf,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GACE,OAAM;GACN,aAAY;GACZ,CAAA,EAEF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,WAAU;gBAAmD;MAE5D,CAAA,EACR,kBAAC,UAAD;MACE,OAAO;MACP,WAAW,MAAU,EAA4B,EAAM,OAAO,MAAM;MACpE,WAAU;gBAET,EAAgB,KAAK,MACpB,kBAAC,UAAD;OAAyB,OAAO,EAAQ;iBACrC,EAAQ;OACF,EAFI,EAAQ,GAEZ,CACT;MACK,CAAA,CACL,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,WAAU;gBAAmD;MAAe,CAAA,EACnF,kBAAC,SAAD;MACE,MAAK;MACL,KAAI;MACJ,MAAK;MACL,OAAO;MACP,WAAW,MAAU,EAAU,EAAM,OAAO,MAAM;MAClD,WAAU;MACV,aAAY;MACZ,CAAA,CACE,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,WAAU;gBAAmD;MAAc,CAAA,EAClF,kBAAC,YAAD;MACE,OAAO;MACP,WAAW,MAAU,EAAU,EAAM,OAAO,MAAM;MAClD,WAAU;MACV,aAAY;MACZ,CAAA,CACE,EAAA,CAAA;KAEL,IAAU,kBAAC,KAAD;MAAG,WAAU;gBAAoC;MAAY,CAAA,GAAG;KAC1E,IAAQ,kBAAC,KAAD;MAAG,WAAU;gBAAkC;MAAU,CAAA,GAAG;KAErE,kBAAC,UAAD;MACE,MAAK;MACL,SAvFiB,YAAY;AACrC,WAAI,CAAC,GAAwB;AAC3B,UAAS,kCAAkC;AAC3C;;AAGF,WAAI,CAAC,KAAU,OAAO,EAAO,IAAI,GAAG;AAClC,UAAS,+BAA+B;AACxC;;AAKF,OAFA,EAAgB,GAAK,EACrB,EAAS,KAAK,EACd,EAAW,KAAK;AAEhB,WAAI;AAUF,QATA,MAAM,EAAa;SACjB,kBAAkB;SAClB,QAAQ,OAAO,EAAO;SACtB;SACD,CAAC,EAEF,MAAM,QAAQ,IAAI,CAAC,GAAS,EAAE,GAAqB,CAAC,CAAC,EACrD,EAAU,GAAG,EACb,EAAU,GAAG,EACb,EAAW,gCAAgC;gBACpC,GAAY;AACnB,UAAS,aAAsB,QAAQ,EAAW,UAAU,2BAA2B;iBAC/E;AACR,UAAgB,GAAM;;;MA2DhB,UAAU,KAAW,KAAgB,EAAgB,WAAW;MAChE,WAAU;gBAET,IAAe,wBAAwB;MACjC,CAAA;KACL;OAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,MAAD;MAAI,WAAU;gBAAkC;MAAyB,CAAA;KACrE,CAAA,EAEL,IACC,kBAAC,OAAD;KAAK,WAAU;eAA8B;KAAqC,CAAA,GAChF,EAAa,WAAW,IAC1B,kBAAC,OAAD;KAAK,WAAU;eAA8B;KAA0C,CAAA,GAEvF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAa,KAAK,MACjB,kBAAC,OAAD;MAA0B,WAAU;gBAApC,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAyC,EAAY;OAAS,CAAA,EAC3E,kBAAC,KAAD;OAAG,WAAU;iBACT,EAAY,SAA6C,UACzD;OACA,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAb,CACG,EAAY,SAAS,IAAI,MAAM,IAC/B,EAAY,OACX;WACJ,kBAAC,KAAD;QAAG,WAAU;kBAA2B,EAAY;QAAW,CAAA,CAC3D;SACF;QAfI,EAAY,GAehB,CACN;KACE,CAAA,CAEJ;MACF;KACF;MAnIC,kBAAC,GAAD,EAAc,SAAQ,+CAAgD,CAAA"}
|
|
@@ -262,7 +262,7 @@ var j = () => {
|
|
|
262
262
|
}, M = ({ transaction: e }) => {
|
|
263
263
|
let t = s(e.status), n = Number(e.amount), r = n > 0, l = Math.abs(n);
|
|
264
264
|
return /* @__PURE__ */ k("div", {
|
|
265
|
-
className: "flex items-center gap-4 p-4 border border-border-
|
|
265
|
+
className: "flex items-center gap-4 p-4 border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 hover:shadow-sm transition-shadow",
|
|
266
266
|
children: [
|
|
267
267
|
/* @__PURE__ */ O("div", {
|
|
268
268
|
className: `size-10 rounded-full flex items-center justify-center flex-shrink-0 ${(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CreditTransactionsPage.js","names":[],"sources":["../../../../../src/billing/modules/credits/pages/CreditTransactionsPage.tsx"],"sourcesContent":["/**\n * Credits Module - Credit Transactions Page\n * Displays credit transaction history for a specific billing account\n */\n\nimport { useState, type FC } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Coins,\n CreditCard,\n Mail,\n ChevronLeft,\n ChevronRight,\n Plus,\n Minus,\n CheckCircle,\n XCircle,\n FileText,\n Calendar,\n} from 'lucide-react';\nimport { useCreditTransactions } from '../hooks/useCredits';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { AccessDenied, PageHeader, EmptyState, ServerError } from '../../../shared/components';\nimport {\n formatDateTime,\n formatRelativeTime,\n formatCreditTransactionType,\n isServerError,\n} from '../../../shared/utils';\nimport {\n getCreditTransactionStatusColor,\n getStatusBadgeClasses,\n} from '../../../shared/utils/status';\nimport type { CreditTransaction, CreditTransactionType } from '../../../shared/types';\nimport { useGetBillingAccountQuery } from '../../../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype FilterType = 'all' | CreditTransactionType;\n\nexport const CreditTransactionsPage: 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 { billingAccountId } = useParams<{ billingAccountId: string }>();\n const permissions = useBillingPermissions();\n const navigateTo = useBillingNavigate();\n\n const [days, setDays] = useState(30);\n const [filterType, setFilterType] = useState<FilterType>('all');\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // Fetch billing account details\n const { data: accountData, loading: isLoadingAccount } = useGetBillingAccountQuery({\n variables: { id: billingAccountId! },\n skip: !billingAccountId,\n });\n\n const billingAccount = accountData?.getBillingAccount;\n\n // Fetch credit transactions\n const {\n transactions,\n pagination,\n isLoading: isLoadingTransactions,\n error,\n refetch,\n } = useCreditTransactions({\n billingAccountId: billingAccountId || '',\n days,\n page,\n pageSize,\n });\n\n // Permission check\n if (!permissions.canViewCredits) {\n return <AccessDenied message=\"You don't have permission to view credit history.\" />;\n }\n\n // Show error if no billing account ID provided\n if (!billingAccountId) {\n return (\n <div className=\"p-6\">\n <EmptyState\n icon={\n <svg\n className=\"size-6 text-text-secondary\"\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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n }\n title=\"No billing account selected\"\n description=\"Please select a billing account from the overview page to view credit history.\"\n />\n </div>\n );\n }\n\n // Show server error if backend is down\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load credit transactions. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n const isLoading = isLoadingAccount || isLoadingTransactions;\n\n // Filter transactions\n const filteredTransactions =\n filterType === 'all' ? transactions : transactions.filter((t) => t.type === filterType);\n\n // Calculate total pages\n const totalPages = Math.ceil(pagination.totalCount / pageSize);\n\n // Pagination handlers\n const goToNextPage = () => {\n if (pagination.hasMore) {\n setPage((p) => p + 1);\n }\n };\n\n const goToPreviousPage = () => {\n if (page > 1) {\n setPage((p) => p - 1);\n }\n };\n\n if (isLoading && !billingAccount) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/overview')}\n className=\"inline-flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.credits.backToOverview', 'Back to Overview')}\n </button>\n\n <PageHeader\n title=\"Credit History\"\n description={\n billingAccount\n ? `Credit transactions for ${billingAccount.name}`\n : 'View your credit transaction history'\n }\n />\n\n {/* Billing Account Info Card */}\n {billingAccount && (\n <div className=\"border border-border-subtle rounded-lg p-4 bg-bg-surface\">\n <div className=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4\">\n {/* Account Name & Status */}\n <div className=\"flex items-start gap-2\">\n <CreditCard className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Account</p>\n <p className=\"text-sm font-medium\">{billingAccount.name}</p>\n {billingAccount.isActive ? (\n <span className=\"inline-flex items-center gap-1 mt-1 px-1.5 py-0.5 text-xs font-medium rounded bg-status-success-bg-subtle text-status-success-text\">\n <CheckCircle className=\"size-3\" />\n Active\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 mt-1 px-1.5 py-0.5 text-xs font-medium rounded bg-status-error-bg-subtle text-status-error-text\">\n <XCircle className=\"size-3\" />\n Inactive\n </span>\n )}\n </div>\n </div>\n\n {/* Email */}\n <div className=\"flex items-start gap-2\">\n <Mail className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Email</p>\n <p className=\"text-sm font-medium break-all\">\n {billingAccount.billingEmail || billingAccount.email}\n </p>\n </div>\n </div>\n\n {/* Credit Balance */}\n <div className=\"flex items-start gap-2\">\n <Coins className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Credit Balance</p>\n <p className=\"text-sm font-semibold text-text-primary\">\n {(billingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\n\n {/* Tax ID */}\n <div className=\"flex items-start gap-2\">\n <FileText className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Tax ID</p>\n <p className=\"text-sm font-medium\">{billingAccount.taxId || '-'}</p>\n </div>\n </div>\n\n {/* Account Created */}\n <div className=\"flex items-start gap-2\">\n <Calendar className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Member Since</p>\n <p className=\"text-sm font-medium\">\n {billingAccount.createdAt ? formatDateTime(billingAccount.createdAt) : '-'}\n </p>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Time Range */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {[7, 30, 90].map((d) => (\n <button\n type=\"button\"\n key={d}\n onClick={() => {\n setDays(d);\n setPage(1); // Reset to first page on filter change\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${\n days === d\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {d}d\n </button>\n ))}\n </div>\n\n {/* Transaction Type Filter */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {(['all', 'purchase', 'usage', 'adjustment'] as FilterType[]).map((type) => (\n <button\n type=\"button\"\n key={type}\n onClick={() => {\n setFilterType(type);\n setPage(1); // Reset to first page on filter change\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${\n filterType === type\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {type === 'all' ? 'All' : formatCreditTransactionType(type as CreditTransactionType)}\n </button>\n ))}\n </div>\n\n {/* Transaction count */}\n <div className=\"ml-auto text-sm text-text-secondary\">\n {pagination.totalCount} transaction{pagination.totalCount !== 1 ? 's' : ''} in last {days}{' '}\n days\n </div>\n </div>\n\n {/* Transactions List */}\n {filteredTransactions.length === 0 ? (\n <EmptyState\n icon={<Coins className=\"size-6 text-text-secondary\" />}\n title=\"No credit transactions found\"\n description={\n filterType !== 'all'\n ? 'No transactions match your filter.'\n : 'Your credit transaction history will appear here.'\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredTransactions.map((transaction) => (\n <CreditTransactionRow key={transaction.id} transaction={transaction} />\n ))}\n </div>\n )}\n\n {/* Pagination Controls */}\n {pagination.totalCount > 0 && (\n <div className=\"flex items-center justify-between border-t border-border-subtle pt-4\">\n <p className=\"text-sm text-text-secondary\">\n Showing {(page - 1) * pageSize + 1} to{' '}\n {Math.min(page * pageSize, pagination.totalCount)} of {pagination.totalCount}{' '}\n transactions\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={goToPreviousPage}\n disabled={page <= 1}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-button border border-border-subtle bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n <ChevronLeft className=\"size-4\" />\n Previous\n </button>\n <span className=\"text-sm text-text-secondary px-2\">\n Page {page} of {totalPages || 1}\n </span>\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!pagination.hasMore}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-button border border-border-subtle bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Next\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Credit Transaction Row Component\n// ============================================================================\n\ninterface CreditTransactionRowProps {\n transaction: CreditTransaction;\n}\n\nconst CreditTransactionRow: FC<CreditTransactionRowProps> = ({ transaction }) => {\n const statusColor = getCreditTransactionStatusColor(transaction.status);\n\n // FIX: Check the actual amount value, not just the transaction type\n // Negative amounts are debits (show red with \"-\"), positive amounts are credits (show green with \"+\")\n const amount = Number(transaction.amount);\n const isCredit = amount > 0;\n const absAmount = Math.abs(amount);\n\n const getTransactionIcon = () => {\n // For adjustments, show different icon based on amount sign\n if (transaction.type === 'adjustment') {\n return isCredit ? (\n <Plus className=\"size-4 text-status-success-text\" />\n ) : (\n <Minus className=\"size-4 text-status-error-text\" />\n );\n }\n\n switch (transaction.type) {\n case 'purchase':\n return <Plus className=\"size-4 text-status-success-text\" />;\n case 'usage':\n return <Minus className=\"size-4 text-status-warning-text\" />;\n default:\n return <Coins className=\"size-4 text-text-secondary\" />;\n }\n };\n\n const getIconBackground = () => {\n // For adjustments, show different background based on amount sign\n if (transaction.type === 'adjustment') {\n return isCredit ? 'bg-status-success-bg-subtle' : 'bg-status-error-bg-subtle';\n }\n\n switch (transaction.type) {\n case 'purchase':\n return 'bg-status-success-bg-subtle';\n case 'usage':\n return 'bg-status-warning-bg-subtle';\n default:\n return 'bg-bg-sunken';\n }\n };\n\n return (\n <div className=\"flex items-center gap-4 p-4 border border-border-subtle rounded-card bg-bg-surface shadow-elevation-1 hover:shadow-sm transition-shadow\">\n {/* Icon */}\n <div\n className={`size-10 rounded-full flex items-center justify-center flex-shrink-0 ${getIconBackground()}`}\n >\n {getTransactionIcon()}\n </div>\n\n {/* Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <p className=\"font-medium text-text-primary capitalize\">\n {formatCreditTransactionType(transaction.type)}\n </p>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {transaction.status}\n </span>\n {transaction.entityType && (\n <span className=\"px-2 py-0.5 text-xs font-medium rounded bg-bg-sunken text-text-secondary capitalize\">\n {transaction.entityType}\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {formatRelativeTime(transaction.createdAt)} • {formatDateTime(transaction.createdAt)}\n </p>\n {transaction.productId && transaction.productId !== 'default' && (\n <p className=\"text-xs text-text-secondary mt-1\">Product: {transaction.productId}</p>\n )}\n </div>\n\n {/* Amount */}\n <div className=\"text-right flex-shrink-0\">\n <p\n className={`font-semibold text-lg ${isCredit ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {isCredit ? '+' : '-'}\n {absAmount.toLocaleString()} credits\n </p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CreditTransactionsPage.js","names":[],"sources":["../../../../../src/billing/modules/credits/pages/CreditTransactionsPage.tsx"],"sourcesContent":["/**\n * Credits Module - Credit Transactions Page\n * Displays credit transaction history for a specific billing account\n */\n\nimport { useState, type FC } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Coins,\n CreditCard,\n Mail,\n ChevronLeft,\n ChevronRight,\n Plus,\n Minus,\n CheckCircle,\n XCircle,\n FileText,\n Calendar,\n} from 'lucide-react';\nimport { useCreditTransactions } from '../hooks/useCredits';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useBillingNavigate } from '../../../hooks/useBillingNavigate';\nimport { AccessDenied, PageHeader, EmptyState, ServerError } from '../../../shared/components';\nimport {\n formatDateTime,\n formatRelativeTime,\n formatCreditTransactionType,\n isServerError,\n} from '../../../shared/utils';\nimport {\n getCreditTransactionStatusColor,\n getStatusBadgeClasses,\n} from '../../../shared/utils/status';\nimport type { CreditTransaction, CreditTransactionType } from '../../../shared/types';\nimport { useGetBillingAccountQuery } from '../../../../generated/global-operations';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\ntype FilterType = 'all' | CreditTransactionType;\n\nexport const CreditTransactionsPage: 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 { billingAccountId } = useParams<{ billingAccountId: string }>();\n const permissions = useBillingPermissions();\n const navigateTo = useBillingNavigate();\n\n const [days, setDays] = useState(30);\n const [filterType, setFilterType] = useState<FilterType>('all');\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // Fetch billing account details\n const { data: accountData, loading: isLoadingAccount } = useGetBillingAccountQuery({\n variables: { id: billingAccountId! },\n skip: !billingAccountId,\n });\n\n const billingAccount = accountData?.getBillingAccount;\n\n // Fetch credit transactions\n const {\n transactions,\n pagination,\n isLoading: isLoadingTransactions,\n error,\n refetch,\n } = useCreditTransactions({\n billingAccountId: billingAccountId || '',\n days,\n page,\n pageSize,\n });\n\n // Permission check\n if (!permissions.canViewCredits) {\n return <AccessDenied message=\"You don't have permission to view credit history.\" />;\n }\n\n // Show error if no billing account ID provided\n if (!billingAccountId) {\n return (\n <div className=\"p-6\">\n <EmptyState\n icon={\n <svg\n className=\"size-6 text-text-secondary\"\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-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n }\n title=\"No billing account selected\"\n description=\"Please select a billing account from the overview page to view credit history.\"\n />\n </div>\n );\n }\n\n // Show server error if backend is down\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load credit transactions. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n const isLoading = isLoadingAccount || isLoadingTransactions;\n\n // Filter transactions\n const filteredTransactions =\n filterType === 'all' ? transactions : transactions.filter((t) => t.type === filterType);\n\n // Calculate total pages\n const totalPages = Math.ceil(pagination.totalCount / pageSize);\n\n // Pagination handlers\n const goToNextPage = () => {\n if (pagination.hasMore) {\n setPage((p) => p + 1);\n }\n };\n\n const goToPreviousPage = () => {\n if (page > 1) {\n setPage((p) => p - 1);\n }\n };\n\n if (isLoading && !billingAccount) {\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"space-y-2\">\n {[1, 2, 3, 4, 5].map((i) => (\n <div key={i} className=\"h-16 bg-bg-sunken animate-pulse rounded\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 px-6 lg:px-8 py-6\">\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={() => navigateTo('/overview')}\n className=\"inline-flex items-center gap-2 text-sm text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('billing.credits.backToOverview', 'Back to Overview')}\n </button>\n\n <PageHeader\n title=\"Credit History\"\n description={\n billingAccount\n ? `Credit transactions for ${billingAccount.name}`\n : 'View your credit transaction history'\n }\n />\n\n {/* Billing Account Info Card */}\n {billingAccount && (\n <div className=\"border border-border-subtle rounded-lg p-4 bg-bg-surface\">\n <div className=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4\">\n {/* Account Name & Status */}\n <div className=\"flex items-start gap-2\">\n <CreditCard className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Account</p>\n <p className=\"text-sm font-medium\">{billingAccount.name}</p>\n {billingAccount.isActive ? (\n <span className=\"inline-flex items-center gap-1 mt-1 px-1.5 py-0.5 text-xs font-medium rounded bg-status-success-bg-subtle text-status-success-text\">\n <CheckCircle className=\"size-3\" />\n Active\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 mt-1 px-1.5 py-0.5 text-xs font-medium rounded bg-status-error-bg-subtle text-status-error-text\">\n <XCircle className=\"size-3\" />\n Inactive\n </span>\n )}\n </div>\n </div>\n\n {/* Email */}\n <div className=\"flex items-start gap-2\">\n <Mail className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Email</p>\n <p className=\"text-sm font-medium break-all\">\n {billingAccount.billingEmail || billingAccount.email}\n </p>\n </div>\n </div>\n\n {/* Credit Balance */}\n <div className=\"flex items-start gap-2\">\n <Coins className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Credit Balance</p>\n <p className=\"text-sm font-semibold text-text-primary\">\n {(billingAccount.creditAmount || 0).toLocaleString()} Credits\n </p>\n </div>\n </div>\n\n {/* Tax ID */}\n <div className=\"flex items-start gap-2\">\n <FileText className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Tax ID</p>\n <p className=\"text-sm font-medium\">{billingAccount.taxId || '-'}</p>\n </div>\n </div>\n\n {/* Account Created */}\n <div className=\"flex items-start gap-2\">\n <Calendar className=\"size-4 text-text-secondary mt-0.5\" />\n <div>\n <p className=\"text-xs text-text-secondary mb-0.5\">Member Since</p>\n <p className=\"text-sm font-medium\">\n {billingAccount.createdAt ? formatDateTime(billingAccount.createdAt) : '-'}\n </p>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"flex flex-wrap items-center gap-4 pb-4 border-b border-border-subtle\">\n {/* Time Range */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {[7, 30, 90].map((d) => (\n <button\n type=\"button\"\n key={d}\n onClick={() => {\n setDays(d);\n setPage(1); // Reset to first page on filter change\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${\n days === d\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {d}d\n </button>\n ))}\n </div>\n\n {/* Transaction Type Filter */}\n <div className=\"inline-flex rounded-lg border border-border-subtle p-1 bg-bg-sunken\">\n {(['all', 'purchase', 'usage', 'adjustment'] as FilterType[]).map((type) => (\n <button\n type=\"button\"\n key={type}\n onClick={() => {\n setFilterType(type);\n setPage(1); // Reset to first page on filter change\n }}\n className={`px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${\n filterType === type\n ? 'bg-bg-surface text-text-primary shadow-sm'\n : 'text-text-secondary hover:text-text-primary'\n }`}\n >\n {type === 'all' ? 'All' : formatCreditTransactionType(type as CreditTransactionType)}\n </button>\n ))}\n </div>\n\n {/* Transaction count */}\n <div className=\"ml-auto text-sm text-text-secondary\">\n {pagination.totalCount} transaction{pagination.totalCount !== 1 ? 's' : ''} in last {days}{' '}\n days\n </div>\n </div>\n\n {/* Transactions List */}\n {filteredTransactions.length === 0 ? (\n <EmptyState\n icon={<Coins className=\"size-6 text-text-secondary\" />}\n title=\"No credit transactions found\"\n description={\n filterType !== 'all'\n ? 'No transactions match your filter.'\n : 'Your credit transaction history will appear here.'\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredTransactions.map((transaction) => (\n <CreditTransactionRow key={transaction.id} transaction={transaction} />\n ))}\n </div>\n )}\n\n {/* Pagination Controls */}\n {pagination.totalCount > 0 && (\n <div className=\"flex items-center justify-between border-t border-border-subtle pt-4\">\n <p className=\"text-sm text-text-secondary\">\n Showing {(page - 1) * pageSize + 1} to{' '}\n {Math.min(page * pageSize, pagination.totalCount)} of {pagination.totalCount}{' '}\n transactions\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={goToPreviousPage}\n disabled={page <= 1}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-button border border-border-subtle bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n <ChevronLeft className=\"size-4\" />\n Previous\n </button>\n <span className=\"text-sm text-text-secondary px-2\">\n Page {page} of {totalPages || 1}\n </span>\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!pagination.hasMore}\n className=\"inline-flex items-center gap-1 px-3 py-1.5 text-sm font-medium rounded-button border border-border-subtle bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Next\n <ChevronRight className=\"size-4\" />\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Credit Transaction Row Component\n// ============================================================================\n\ninterface CreditTransactionRowProps {\n transaction: CreditTransaction;\n}\n\nconst CreditTransactionRow: FC<CreditTransactionRowProps> = ({ transaction }) => {\n const statusColor = getCreditTransactionStatusColor(transaction.status);\n\n // FIX: Check the actual amount value, not just the transaction type\n // Negative amounts are debits (show red with \"-\"), positive amounts are credits (show green with \"+\")\n const amount = Number(transaction.amount);\n const isCredit = amount > 0;\n const absAmount = Math.abs(amount);\n\n const getTransactionIcon = () => {\n // For adjustments, show different icon based on amount sign\n if (transaction.type === 'adjustment') {\n return isCredit ? (\n <Plus className=\"size-4 text-status-success-text\" />\n ) : (\n <Minus className=\"size-4 text-status-error-text\" />\n );\n }\n\n switch (transaction.type) {\n case 'purchase':\n return <Plus className=\"size-4 text-status-success-text\" />;\n case 'usage':\n return <Minus className=\"size-4 text-status-warning-text\" />;\n default:\n return <Coins className=\"size-4 text-text-secondary\" />;\n }\n };\n\n const getIconBackground = () => {\n // For adjustments, show different background based on amount sign\n if (transaction.type === 'adjustment') {\n return isCredit ? 'bg-status-success-bg-subtle' : 'bg-status-error-bg-subtle';\n }\n\n switch (transaction.type) {\n case 'purchase':\n return 'bg-status-success-bg-subtle';\n case 'usage':\n return 'bg-status-warning-bg-subtle';\n default:\n return 'bg-bg-sunken';\n }\n };\n\n return (\n <div className=\"flex items-center gap-4 p-4 border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 hover:shadow-sm transition-shadow\">\n {/* Icon */}\n <div\n className={`size-10 rounded-full flex items-center justify-center flex-shrink-0 ${getIconBackground()}`}\n >\n {getTransactionIcon()}\n </div>\n\n {/* Info */}\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <p className=\"font-medium text-text-primary capitalize\">\n {formatCreditTransactionType(transaction.type)}\n </p>\n <span\n className={`px-2 py-0.5 text-xs font-medium rounded ${getStatusBadgeClasses(statusColor)}`}\n >\n {transaction.status}\n </span>\n {transaction.entityType && (\n <span className=\"px-2 py-0.5 text-xs font-medium rounded bg-bg-sunken text-text-secondary capitalize\">\n {transaction.entityType}\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">\n {formatRelativeTime(transaction.createdAt)} • {formatDateTime(transaction.createdAt)}\n </p>\n {transaction.productId && transaction.productId !== 'default' && (\n <p className=\"text-xs text-text-secondary mt-1\">Product: {transaction.productId}</p>\n )}\n </div>\n\n {/* Amount */}\n <div className=\"text-right flex-shrink-0\">\n <p\n className={`font-semibold text-lg ${isCredit ? 'text-status-success-text' : 'text-status-error-text'}`}\n >\n {isCredit ? '+' : '-'}\n {absAmount.toLocaleString()} credits\n </p>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -9,15 +9,16 @@ import "../../subscriptions/hooks/index.js";
|
|
|
9
9
|
import { useState as u } from "react";
|
|
10
10
|
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
11
|
import { useI18n as p } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
12
|
+
import { EmphasisPanel as m } from "@burdenoff/fe-libs/ui";
|
|
12
13
|
//#region src/billing/modules/credits/pages/CreditsPage.tsx
|
|
13
|
-
var
|
|
14
|
+
var h = () => {
|
|
14
15
|
let { t: e } = p(), n = (t, n) => {
|
|
15
16
|
let r = e(t);
|
|
16
17
|
return r === t ? n : r;
|
|
17
|
-
}, r = s(), i = o(), { billingAccount: a, isLoading:
|
|
18
|
+
}, r = s(), i = o(), { billingAccount: a, isLoading: h } = l(), [_, v] = u(30), [y, b] = u("all"), [x, S] = u(1), { transactions: C, pagination: w, isLoading: T, error: E, refetch: D } = c({
|
|
18
19
|
billingAccountId: a?.id || "",
|
|
19
|
-
days:
|
|
20
|
-
page:
|
|
20
|
+
days: _,
|
|
21
|
+
page: x,
|
|
21
22
|
pageSize: 20
|
|
22
23
|
});
|
|
23
24
|
if (!i.canViewCredits) return /* @__PURE__ */ d("div", {
|
|
@@ -51,8 +52,8 @@ var m = () => {
|
|
|
51
52
|
]
|
|
52
53
|
})
|
|
53
54
|
});
|
|
54
|
-
let
|
|
55
|
-
if (
|
|
55
|
+
let O = h || T, k = C.filter((e) => y === "all" ? !0 : e.type === y);
|
|
56
|
+
if (O && !a) return /* @__PURE__ */ f("div", {
|
|
56
57
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
57
58
|
children: [/* @__PURE__ */ d("div", { className: "h-8 w-48 bg-bg-sunken animate-pulse rounded" }), /* @__PURE__ */ d("div", {
|
|
58
59
|
className: "grid grid-cols-1 sm:grid-cols-2 gap-4",
|
|
@@ -62,7 +63,7 @@ var m = () => {
|
|
|
62
63
|
})
|
|
63
64
|
})]
|
|
64
65
|
});
|
|
65
|
-
if (
|
|
66
|
+
if (E) return /* @__PURE__ */ d("div", {
|
|
66
67
|
className: "flex items-center justify-center h-full min-h-[400px]",
|
|
67
68
|
children: /* @__PURE__ */ f("div", {
|
|
68
69
|
className: "text-center space-y-4",
|
|
@@ -88,18 +89,18 @@ var m = () => {
|
|
|
88
89
|
}),
|
|
89
90
|
/* @__PURE__ */ d("p", {
|
|
90
91
|
className: "text-sm text-text-secondary",
|
|
91
|
-
children:
|
|
92
|
+
children: E.message
|
|
92
93
|
}),
|
|
93
94
|
/* @__PURE__ */ d("button", {
|
|
94
95
|
type: "button",
|
|
95
|
-
onClick: () =>
|
|
96
|
+
onClick: () => D(),
|
|
96
97
|
className: "px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-button hover:bg-action-primary-bgHover transition-colors",
|
|
97
98
|
children: "Try Again"
|
|
98
99
|
})
|
|
99
100
|
]
|
|
100
101
|
})
|
|
101
102
|
});
|
|
102
|
-
let
|
|
103
|
+
let A = a?.creditAmount || 0;
|
|
103
104
|
return /* @__PURE__ */ f("div", {
|
|
104
105
|
className: "space-y-6 px-6 lg:px-8 py-6",
|
|
105
106
|
children: [
|
|
@@ -150,16 +151,16 @@ var m = () => {
|
|
|
150
151
|
}),
|
|
151
152
|
/* @__PURE__ */ f("div", {
|
|
152
153
|
className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4",
|
|
153
|
-
children: [/* @__PURE__ */ f(
|
|
154
|
-
className: "
|
|
154
|
+
children: [/* @__PURE__ */ f(m, {
|
|
155
|
+
className: "p-6",
|
|
155
156
|
children: [
|
|
156
157
|
/* @__PURE__ */ d("p", {
|
|
157
158
|
className: "text-sm text-text-secondary",
|
|
158
159
|
children: "Available Balance"
|
|
159
160
|
}),
|
|
160
161
|
/* @__PURE__ */ f("p", {
|
|
161
|
-
className: "text-3xl font-bold text-text-primary mt-2",
|
|
162
|
-
children: [
|
|
162
|
+
className: "text-3xl font-bold tabular-nums text-text-primary mt-2",
|
|
163
|
+
children: [A.toLocaleString(), " Credits"]
|
|
163
164
|
}),
|
|
164
165
|
/* @__PURE__ */ d("p", {
|
|
165
166
|
className: "text-sm text-text-secondary mt-2",
|
|
@@ -167,19 +168,19 @@ var m = () => {
|
|
|
167
168
|
})
|
|
168
169
|
]
|
|
169
170
|
}), /* @__PURE__ */ f("div", {
|
|
170
|
-
className: "border border-border-
|
|
171
|
+
className: "border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 p-5",
|
|
171
172
|
children: [
|
|
172
173
|
/* @__PURE__ */ f("p", {
|
|
173
174
|
className: "text-sm text-text-secondary",
|
|
174
175
|
children: [
|
|
175
176
|
"Transactions (",
|
|
176
|
-
|
|
177
|
+
_,
|
|
177
178
|
"d)"
|
|
178
179
|
]
|
|
179
180
|
}),
|
|
180
181
|
/* @__PURE__ */ d("p", {
|
|
181
182
|
className: "text-2xl md:text-3xl font-semibold tracking-tight text-text-primary mt-2",
|
|
182
|
-
children:
|
|
183
|
+
children: w.totalCount
|
|
183
184
|
}),
|
|
184
185
|
/* @__PURE__ */ d("p", {
|
|
185
186
|
className: "text-sm text-text-secondary mt-2",
|
|
@@ -198,8 +199,8 @@ var m = () => {
|
|
|
198
199
|
90
|
|
199
200
|
].map((e) => /* @__PURE__ */ f("button", {
|
|
200
201
|
type: "button",
|
|
201
|
-
onClick: () =>
|
|
202
|
-
className: `px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${
|
|
202
|
+
onClick: () => v(e),
|
|
203
|
+
className: `px-3 py-1.5 text-sm font-medium rounded-button transition-colors ${_ === e ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"}`,
|
|
203
204
|
children: [e, "d"]
|
|
204
205
|
}, e))
|
|
205
206
|
}), /* @__PURE__ */ d("div", {
|
|
@@ -212,13 +213,13 @@ var m = () => {
|
|
|
212
213
|
"granted"
|
|
213
214
|
].map((e) => /* @__PURE__ */ d("button", {
|
|
214
215
|
type: "button",
|
|
215
|
-
onClick: () =>
|
|
216
|
-
className: `px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${
|
|
216
|
+
onClick: () => b(e),
|
|
217
|
+
className: `px-3 py-1.5 text-sm font-medium rounded-button transition-colors capitalize ${y === e ? "bg-bg-surface text-text-primary shadow-sm" : "text-text-secondary hover:text-text-primary"}`,
|
|
217
218
|
children: e === "all" ? "All" : t(e)
|
|
218
219
|
}, e))
|
|
219
220
|
})]
|
|
220
221
|
}),
|
|
221
|
-
|
|
222
|
+
k.length === 0 ? /* @__PURE__ */ f("div", {
|
|
222
223
|
className: "flex flex-col items-center justify-center py-12 border border-dashed border-border-subtle rounded-lg",
|
|
223
224
|
children: [
|
|
224
225
|
/* @__PURE__ */ d("div", {
|
|
@@ -242,16 +243,16 @@ var m = () => {
|
|
|
242
243
|
}),
|
|
243
244
|
/* @__PURE__ */ d("p", {
|
|
244
245
|
className: "text-sm text-text-secondary mt-1",
|
|
245
|
-
children:
|
|
246
|
+
children: y === "all" ? "No credit transactions in this period." : "No transactions match your filter."
|
|
246
247
|
})
|
|
247
248
|
]
|
|
248
249
|
}) : /* @__PURE__ */ f("div", {
|
|
249
250
|
className: "space-y-3",
|
|
250
|
-
children: [
|
|
251
|
+
children: [k.map((e) => /* @__PURE__ */ d(g, { transaction: e }, e.id)), w.hasMore && /* @__PURE__ */ d("div", {
|
|
251
252
|
className: "flex justify-center pt-4",
|
|
252
253
|
children: /* @__PURE__ */ d("button", {
|
|
253
254
|
type: "button",
|
|
254
|
-
onClick: () =>
|
|
255
|
+
onClick: () => S((e) => e + 1),
|
|
255
256
|
className: "px-4 py-2 text-sm font-medium border border-border-subtle text-text-primary rounded-button hover:bg-bg-sunken transition-colors",
|
|
256
257
|
children: "Load More"
|
|
257
258
|
})
|
|
@@ -259,10 +260,10 @@ var m = () => {
|
|
|
259
260
|
})
|
|
260
261
|
]
|
|
261
262
|
});
|
|
262
|
-
},
|
|
263
|
+
}, g = ({ transaction: o }) => {
|
|
263
264
|
let s = i(o.status), c = o.type === "purchase" || o.type === "adjustment" || o.type === "granted";
|
|
264
265
|
return /* @__PURE__ */ f("div", {
|
|
265
|
-
className: "flex items-center gap-4 p-4 border border-border-
|
|
266
|
+
className: "flex items-center gap-4 p-4 border border-border-seam rounded-card bg-bg-surface shadow-elevation-1 hover:shadow-sm transition-shadow",
|
|
266
267
|
children: [
|
|
267
268
|
/* @__PURE__ */ d("div", {
|
|
268
269
|
className: `size-10 rounded-full flex items-center justify-center ${c ? e.success.bg : e.warning.bg}`,
|
|
@@ -325,6 +326,6 @@ var m = () => {
|
|
|
325
326
|
});
|
|
326
327
|
};
|
|
327
328
|
//#endregion
|
|
328
|
-
export {
|
|
329
|
+
export { h as CreditsPage };
|
|
329
330
|
|
|
330
331
|
//# sourceMappingURL=CreditsPage.js.map
|