@burdenoff/microfe-billing 2026.912.6 → 2026.914.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/BillingAdminRoutes.js +100 -74
- package/dist/billing/BillingAdminRoutes.js.map +1 -1
- package/dist/billing/modules/billing/hooks/useRefunds.js.map +1 -1
- package/dist/billing/modules/billing/index.js +5 -4
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js +90 -87
- package/dist/billing/modules/billing/pages/PaymentMethodsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/RefundIssueLogsPage.js +240 -0
- package/dist/billing/modules/billing/pages/RefundIssueLogsPage.js.map +1 -0
- package/dist/billing/modules/billing/pages/RefundsPage.js +116 -108
- package/dist/billing/modules/billing/pages/RefundsPage.js.map +1 -1
- package/dist/billing/modules/billing/pages/index.js +1 -0
- package/dist/billing/modules/checkout/components/OrderSummary.js +22 -19
- package/dist/billing/modules/checkout/components/OrderSummary.js.map +1 -1
- package/dist/billing/modules/checkout/components/RazorpayPaymentButton.js.map +1 -1
- package/dist/billing/modules/checkout/hooks/useCheckout.js +174 -157
- package/dist/billing/modules/checkout/hooks/useCheckout.js.map +1 -1
- package/dist/billing/modules/checkout/pages/CheckoutPage.js +189 -182
- package/dist/billing/modules/checkout/pages/CheckoutPage.js.map +1 -1
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js +293 -274
- package/dist/billing/modules/credits/pages/CreditWithdrawalPage.js.map +1 -1
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js +140 -250
- package/dist/billing/modules/dashboard/components/AddPaymentMethodModal.js.map +1 -1
- package/dist/billing/modules/earnings/api.js +79 -42
- package/dist/billing/modules/earnings/api.js.map +1 -1
- package/dist/billing/modules/earnings/index.js +4 -3
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js +103 -310
- package/dist/billing/modules/earnings/pages/DeveloperRevenueAdminPage.js.map +1 -1
- package/dist/billing/modules/earnings/pages/DeveloperRevenueOrgDetailPage.js +545 -0
- package/dist/billing/modules/earnings/pages/DeveloperRevenueOrgDetailPage.js.map +1 -0
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js +246 -196
- package/dist/billing/modules/earnings/pages/PayoutAdminPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js +44 -28
- package/dist/billing/modules/subscriptions/hooks/useSubscriptions.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js +317 -270
- package/dist/billing/modules/subscriptions/pages/SubscriptionDetailPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js +153 -150
- package/dist/billing/modules/subscriptions/pages/SubscriptionsListPage.js.map +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js +1 -1
- package/dist/billing/modules/subscriptions/pages/UpgradeSubscriptionPage.js.map +1 -1
- package/dist/billing/modules/usage/hooks/useUsage.js +74 -48
- package/dist/billing/modules/usage/hooks/useUsage.js.map +1 -1
- package/dist/generated/global-operations.js +221 -183
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js +163 -0
- package/dist/node_modules/sonner/dist/index.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PaymentMethodsPage.js","names":[],"sources":["../../../../../src/billing/modules/billing/pages/PaymentMethodsPage.tsx"],"sourcesContent":["/**\n * Billing Module - Payment Methods Page\n * Displays and manages payment methods\n */\n\nimport { useCallback, useEffect, useState, type FC } from 'react';\nimport { useLocation } from 'react-router';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useDashboardMutations, useDefaultDashboardAccount } from '../../dashboard/hooks';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { AddPaymentMethodModal } from '../../dashboard/components/AddPaymentMethodModal';\nimport { nativeConfirm, nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\nimport type { PaymentMethod, PaymentMethodType } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\nconst CARD_ICONS: Record<string, string> = {\n visa: '💳',\n mastercard: '💳',\n amex: '💳',\n default: '💳',\n};\n\nconst METHOD_TYPE_LABELS: Record<PaymentMethodType, string> = {\n CREDIT_CARD: 'Credit Card',\n DEBIT_CARD: 'Debit Card',\n WALLET: 'Digital Wallet',\n NETBANKING: 'Net Banking',\n BANK_TRANSFER: 'Bank Transfer',\n PAYPAL: 'PayPal',\n CRYPTO: 'Cryptocurrency',\n};\n\nexport const PaymentMethodsPage: 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 permissions = useBillingPermissions();\n const { emit } = useBillingEventEmitter();\n const { navigate, basePath } = useBilling();\n const { pathname } = useLocation();\n const { billingAccount, paymentMethods, isLoading, refetch } = useDefaultDashboardAccount();\n const { setDefaultPaymentMethod, deletePaymentMethod } = useDashboardMutations();\n const [showAddModal, setShowAddModal] = useState(false);\n const [actionError, setActionError] = useState<string | null>(null);\n\n // basePath-aware path join, mirroring OverviewPage's navigateTo helper.\n const toFullPath = useCallback(\n (path: string) => {\n if (basePath && basePath !== '/') {\n const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n return `${base}/${path}`;\n }\n return `/${path}`;\n },\n [basePath]\n );\n\n // Navigate (basePath-aware) to the dedicated add-payment-method route.\n const navigateToAdd = useCallback(() => {\n navigate(toFullPath('payment-methods/add'));\n }, [navigate, toFullPath]);\n\n const handleSetDefault = useCallback(\n async (paymentMethodId: string): Promise<void> => {\n const billingAccountId = billingAccount?.id;\n if (!billingAccountId) {\n setActionError('No billing account context');\n return;\n }\n\n setActionError(null);\n try {\n await setDefaultPaymentMethod(paymentMethodId, billingAccountId);\n await refetch();\n void nativeNotify('success');\n emit('billing.payment_method.default_set', {\n route: '/billing/payment-methods',\n entityId: paymentMethodId,\n source: 'payment-methods',\n });\n } catch (error) {\n setActionError(\n error instanceof Error ? error.message : 'Failed to set default payment method'\n );\n void nativeNotify('error');\n }\n },\n [billingAccount?.id, emit, refetch, setDefaultPaymentMethod]\n );\n\n const handleDelete = useCallback(\n async (paymentMethodId: string): Promise<void> => {\n const billingAccountId = billingAccount?.id;\n if (!billingAccountId) {\n setActionError('No billing account context');\n return;\n }\n\n setActionError(null);\n try {\n await deletePaymentMethod(paymentMethodId, billingAccountId);\n await refetch();\n void nativeNotify('success');\n emit('billing.payment_method.deleted', {\n route: '/billing/payment-methods',\n entityId: paymentMethodId,\n source: 'payment-methods',\n });\n } catch (error) {\n setActionError(error instanceof Error ? error.message : 'Failed to remove payment method');\n void nativeNotify('error');\n }\n },\n [billingAccount?.id, deletePaymentMethod, emit, refetch]\n );\n\n // The /payment-methods/add route renders this same page; open the add modal\n // automatically when landing there so the route is actually functional.\n useEffect(() => {\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n setShowAddModal(true);\n }\n }, [pathname]);\n\n if (!permissions.canViewPaymentMethods) {\n return (\n <AccessDenied\n message={tr(\n 'billing.paymentMethods.noViewPermission',\n \"You don't have permission to view payment methods.\"\n )}\n />\n );\n }\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-32 bg-bg-sunken animate-pulse rounded-lg\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.paymentMethods.title', 'Payment Methods')}\n description={tr('billing.paymentMethods.subtitle', 'Manage your saved payment methods')}\n actions={\n permissions.canManagePaymentMethods && (\n <button\n type=\"button\"\n onClick={navigateToAdd}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n {tr('billing.paymentMethods.add', 'Add payment method')}\n </button>\n )\n }\n />\n\n <PagePurpose>\n {tr(\n 'billing.paymentMethods.purpose',\n 'Save and manage the cards and payment options used to pay invoices, subscriptions and one-off purchases. Add a method, mark one as default for automatic billing, or remove ones you no longer use.'\n )}\n </PagePurpose>\n\n {actionError && (\n <div className=\"rounded-button border border-status-error-border bg-status-error-bg-subtle p-3\">\n <p className=\"text-sm text-status-error-text\">{actionError}</p>\n </div>\n )}\n\n {/* Payment Methods List */}\n {paymentMethods.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr('billing.paymentMethods.noPaymentMethods', 'No payment methods')}\n description={tr(\n 'billing.paymentMethods.noPaymentMethodsDesc',\n 'Add a payment method to make purchases and manage subscriptions.'\n )}\n action={\n permissions.canManagePaymentMethods ? (\n <button\n type=\"button\"\n onClick={() => setShowAddModal(true)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90\"\n >\n Add Payment Method\n </button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {paymentMethods.map((method) => (\n <PaymentMethodCard\n key={method.id}\n method={method}\n canManage={permissions.canManagePaymentMethods}\n onSetDefault={handleSetDefault}\n onDelete={handleDelete}\n />\n ))}\n </div>\n )}\n\n {/* Add Payment Method Modal (Stripe + Razorpay) */}\n <AddPaymentMethodModal\n isOpen={showAddModal}\n onClose={() => {\n setShowAddModal(false);\n // If we arrived via the /add route, return to the list URL.\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n navigate(toFullPath('payment-methods'));\n }\n }}\n billingAccountId={billingAccount?.id ?? ''}\n currency={billingAccount?.currency ?? 'USD'}\n country={billingAccount?.billingAddresses?.[0]?.country ?? undefined}\n onSuccess={() => {\n emit('billing.payment_method.added', {\n route: '/billing/payment-methods',\n entityId: billingAccount?.id,\n source: 'payment-methods',\n });\n // Refetch billing account to get updated payment methods\n setActionError(null);\n void refetch();\n setShowAddModal(false);\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n navigate(toFullPath('payment-methods'));\n }\n }}\n />\n </div>\n );\n};\n\n// ============================================================================\n// Payment Method Card Component\n// ============================================================================\n\ninterface PaymentMethodCardProps {\n method: PaymentMethod;\n canManage: boolean;\n onSetDefault: (id: string) => Promise<void>;\n onDelete: (id: string) => Promise<void>;\n}\n\nconst PaymentMethodCard: FC<PaymentMethodCardProps> = ({\n method,\n canManage,\n onSetDefault,\n onDelete,\n}) => {\n const [isSettingDefault, setIsSettingDefault] = useState(false);\n const [isDeleting, setIsDeleting] = useState(false);\n const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);\n const isExpired =\n method.expiryYear && method.expiryMonth\n ? new Date(method.expiryYear, method.expiryMonth - 1) < new Date()\n : false;\n\n const handleSetDefault = async () => {\n setIsSettingDefault(true);\n try {\n await onSetDefault(method.id);\n } finally {\n setIsSettingDefault(false);\n }\n };\n\n const handleDelete = async () => {\n setIsDeleting(true);\n try {\n await onDelete(method.id);\n } finally {\n setIsDeleting(false);\n setIsConfirmingDelete(false);\n }\n };\n\n const handleDeleteRequest = async () => {\n void nativeImpact('medium');\n const confirmed = await nativeConfirm({\n title: 'Delete payment method',\n message: 'Are you sure you want to remove this payment method?',\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n\n if (confirmed === true) {\n await handleDelete();\n } else if (confirmed === null) {\n setIsConfirmingDelete(true);\n }\n };\n\n return (\n <div\n className={`border rounded-lg bg-bg-surface p-4 ${\n method.isDefault ? 'border-primary' : 'border-border-subtle'\n }`}\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{CARD_ICONS[method.brand?.toLowerCase() ?? 'default']}</span>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {method.brand ?? METHOD_TYPE_LABELS[method.type]} •••• {method.last4}\n </p>\n {method.isDefault && (\n <span className=\"px-1.5 py-0.5 text-xs font-medium bg-action-primary-bg/10 text-primary rounded\">\n Default\n </span>\n )}\n </div>\n {method.expiryMonth && method.expiryYear && (\n <p\n className={`text-sm ${isExpired ? 'text-status-error-text' : 'text-text-secondary'}`}\n >\n {isExpired ? 'Expired' : 'Expires'} {method.expiryMonth}/{method.expiryYear}\n </p>\n )}\n </div>\n </div>\n </div>\n\n {canManage && (\n <div className=\"flex items-center gap-2 mt-4 pt-4 border-t border-border-subtle\">\n {!method.isDefault && (\n <button\n type=\"button\"\n onClick={handleSetDefault}\n disabled={isSettingDefault || isDeleting}\n className=\"text-sm text-primary hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isSettingDefault ? 'Setting default…' : 'Set as default'}\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleDeleteRequest}\n disabled={isSettingDefault || isDeleting}\n className=\"text-sm text-status-error-text hover:underline ml-auto disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isDeleting ? 'Removing…' : 'Remove'}\n </button>\n </div>\n )}\n\n {isConfirmingDelete && (\n <div className=\"mt-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3\">\n <p className=\"text-sm font-medium text-status-error-text\">\n Remove this payment method from your billing account?\n </p>\n <div className=\"mt-3 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setIsConfirmingDelete(false)}\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={handleDelete}\n disabled={isDeleting}\n className=\"rounded-button bg-action-danger-bg px-3 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-danger-bgHover disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isDeleting ? 'Removing…' : 'Remove'}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PaymentMethodsPage.js","names":[],"sources":["../../../../../src/billing/modules/billing/pages/PaymentMethodsPage.tsx"],"sourcesContent":["/**\n * Billing Module - Payment Methods Page\n * Displays and manages payment methods\n */\n\nimport { useCallback, useEffect, useState, type FC } from 'react';\nimport { useLocation } from 'react-router';\nimport { toast } from 'sonner';\nimport { useBilling } from '../../../providers/BillingProvider';\nimport { useBillingEventEmitter } from '../../../hooks/useBillingEventEmitter';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useDashboardMutations, useDefaultDashboardAccount } from '../../dashboard/hooks';\nimport { AccessDenied, PageHeader } from '../../../shared/components';\nimport { AddPaymentMethodModal } from '../../dashboard/components/AddPaymentMethodModal';\nimport { nativeConfirm, nativeImpact, nativeNotify } from '../../../../utils/nativeBridge';\nimport type { PaymentMethod, PaymentMethodType } from '../../../shared/types';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\n\nconst CARD_ICONS: Record<string, string> = {\n visa: '💳',\n mastercard: '💳',\n amex: '💳',\n default: '💳',\n};\n\nconst METHOD_TYPE_LABELS: Record<PaymentMethodType, string> = {\n CREDIT_CARD: 'Credit Card',\n DEBIT_CARD: 'Debit Card',\n WALLET: 'Digital Wallet',\n NETBANKING: 'Net Banking',\n BANK_TRANSFER: 'Bank Transfer',\n PAYPAL: 'PayPal',\n CRYPTO: 'Cryptocurrency',\n};\n\nexport const PaymentMethodsPage: 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 permissions = useBillingPermissions();\n const { emit } = useBillingEventEmitter();\n const { navigate, basePath } = useBilling();\n const { pathname } = useLocation();\n const { billingAccount, paymentMethods, isLoading, refetch } = useDefaultDashboardAccount();\n const { setDefaultPaymentMethod, deletePaymentMethod } = useDashboardMutations();\n const [showAddModal, setShowAddModal] = useState(false);\n const [actionError, setActionError] = useState<string | null>(null);\n\n // basePath-aware path join, mirroring OverviewPage's navigateTo helper.\n const toFullPath = useCallback(\n (path: string) => {\n if (basePath && basePath !== '/') {\n const base = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;\n return `${base}/${path}`;\n }\n return `/${path}`;\n },\n [basePath]\n );\n\n // Navigate (basePath-aware) to the dedicated add-payment-method route.\n const navigateToAdd = useCallback(() => {\n navigate(toFullPath('payment-methods/add'));\n }, [navigate, toFullPath]);\n\n const handleSetDefault = useCallback(\n async (paymentMethodId: string): Promise<void> => {\n const billingAccountId = billingAccount?.id;\n if (!billingAccountId) {\n setActionError('No billing account context');\n return;\n }\n\n setActionError(null);\n try {\n await setDefaultPaymentMethod(paymentMethodId, billingAccountId);\n await refetch();\n void nativeNotify('success');\n emit('billing.payment_method.default_set', {\n route: '/billing/payment-methods',\n entityId: paymentMethodId,\n source: 'payment-methods',\n });\n } catch (error) {\n setActionError(\n error instanceof Error ? error.message : 'Failed to set default payment method'\n );\n void nativeNotify('error');\n }\n },\n [billingAccount?.id, emit, refetch, setDefaultPaymentMethod]\n );\n\n const handleDelete = useCallback(\n async (paymentMethodId: string): Promise<void> => {\n const billingAccountId = billingAccount?.id;\n if (!billingAccountId) {\n setActionError('No billing account context');\n return;\n }\n\n setActionError(null);\n try {\n await deletePaymentMethod(paymentMethodId, billingAccountId);\n await refetch();\n void nativeNotify('success');\n emit('billing.payment_method.deleted', {\n route: '/billing/payment-methods',\n entityId: paymentMethodId,\n source: 'payment-methods',\n });\n } catch (error) {\n setActionError(error instanceof Error ? error.message : 'Failed to remove payment method');\n void nativeNotify('error');\n }\n },\n [billingAccount?.id, deletePaymentMethod, emit, refetch]\n );\n\n // The /payment-methods/add route renders this same page; open the add modal\n // automatically when landing there so the route is actually functional.\n useEffect(() => {\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n setShowAddModal(true);\n }\n }, [pathname]);\n\n if (!permissions.canViewPaymentMethods) {\n return (\n <AccessDenied\n message={tr(\n 'billing.paymentMethods.noViewPermission',\n \"You don't have permission to view payment methods.\"\n )}\n />\n );\n }\n\n if (isLoading) {\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"h-8 w-48 bg-bg-sunken animate-pulse rounded\" />\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {[1, 2, 3].map((i) => (\n <div key={i} className=\"h-32 bg-bg-sunken animate-pulse rounded-lg\" />\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title={tr('billing.paymentMethods.title', 'Payment Methods')}\n description={tr('billing.paymentMethods.subtitle', 'Manage your saved payment methods')}\n actions={\n permissions.canManagePaymentMethods && (\n <button\n type=\"button\"\n onClick={navigateToAdd}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90 transition-colors\"\n >\n {tr('billing.paymentMethods.add', 'Add payment method')}\n </button>\n )\n }\n />\n\n <PagePurpose>\n {tr(\n 'billing.paymentMethods.purpose',\n 'Save and manage the cards and payment options used to pay invoices, subscriptions and one-off purchases. Add a method, mark one as default for automatic billing, or remove ones you no longer use.'\n )}\n </PagePurpose>\n\n {actionError && (\n <div className=\"rounded-button border border-status-error-border bg-status-error-bg-subtle p-3\">\n <p className=\"text-sm text-status-error-text\">{actionError}</p>\n </div>\n )}\n\n {/* Payment Methods List */}\n {paymentMethods.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr('billing.paymentMethods.noPaymentMethods', 'No payment methods')}\n description={tr(\n 'billing.paymentMethods.noPaymentMethodsDesc',\n 'Add a payment method to make purchases and manage subscriptions.'\n )}\n action={\n permissions.canManagePaymentMethods ? (\n <button\n type=\"button\"\n onClick={() => setShowAddModal(true)}\n className=\"px-4 py-2 text-sm font-medium bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg/90\"\n >\n Add Payment Method\n </button>\n ) : undefined\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4\">\n {paymentMethods.map((method) => (\n <PaymentMethodCard\n key={method.id}\n method={method}\n canManage={permissions.canManagePaymentMethods}\n onSetDefault={handleSetDefault}\n onDelete={handleDelete}\n />\n ))}\n </div>\n )}\n\n {/* Add Payment Method Modal (Stripe + Razorpay) */}\n <AddPaymentMethodModal\n isOpen={showAddModal}\n onClose={() => {\n setShowAddModal(false);\n // If we arrived via the /add route, return to the list URL.\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n navigate(toFullPath('payment-methods'));\n }\n }}\n billingAccountId={billingAccount?.id ?? ''}\n currency={billingAccount?.currency ?? 'USD'}\n country={billingAccount?.billingAddresses?.[0]?.country ?? undefined}\n onSuccess={() => {\n emit('billing.payment_method.added', {\n route: '/billing/payment-methods',\n entityId: billingAccount?.id,\n source: 'payment-methods',\n });\n setActionError(null);\n void nativeNotify('success');\n toast.success('Payment method added successfully');\n setShowAddModal(false);\n if (pathname.replace(/\\/+$/, '').endsWith('/payment-methods/add')) {\n navigate(toFullPath('payment-methods'));\n }\n // The new PaymentMethod row is created by the provider's webhook\n // (e.g. Stripe's setup_intent.succeeded), which can land a beat\n // after the client-side confirm resolves — a single immediate\n // refetch can race it and show the stale list. Refetch again\n // shortly after as a safety net so the new card reliably appears\n // without the user needing to manually reload the page.\n void refetch();\n setTimeout(() => {\n void refetch();\n }, 2500);\n }}\n />\n </div>\n );\n};\n\n// ============================================================================\n// Payment Method Card Component\n// ============================================================================\n\ninterface PaymentMethodCardProps {\n method: PaymentMethod;\n canManage: boolean;\n onSetDefault: (id: string) => Promise<void>;\n onDelete: (id: string) => Promise<void>;\n}\n\nconst PaymentMethodCard: FC<PaymentMethodCardProps> = ({\n method,\n canManage,\n onSetDefault,\n onDelete,\n}) => {\n const [isSettingDefault, setIsSettingDefault] = useState(false);\n const [isDeleting, setIsDeleting] = useState(false);\n const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);\n const isExpired =\n method.expiryYear && method.expiryMonth\n ? new Date(method.expiryYear, method.expiryMonth - 1) < new Date()\n : false;\n\n const handleSetDefault = async () => {\n setIsSettingDefault(true);\n try {\n await onSetDefault(method.id);\n } finally {\n setIsSettingDefault(false);\n }\n };\n\n const handleDelete = async () => {\n setIsDeleting(true);\n try {\n await onDelete(method.id);\n } finally {\n setIsDeleting(false);\n setIsConfirmingDelete(false);\n }\n };\n\n const handleDeleteRequest = async () => {\n void nativeImpact('medium');\n const confirmed = await nativeConfirm({\n title: 'Delete payment method',\n message: 'Are you sure you want to remove this payment method?',\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n\n if (confirmed === true) {\n await handleDelete();\n } else if (confirmed === null) {\n setIsConfirmingDelete(true);\n }\n };\n\n return (\n <div\n className={`border rounded-lg bg-bg-surface p-4 ${\n method.isDefault ? 'border-primary' : 'border-border-subtle'\n }`}\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-2xl\">{CARD_ICONS[method.brand?.toLowerCase() ?? 'default']}</span>\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {method.brand ?? METHOD_TYPE_LABELS[method.type]} •••• {method.last4}\n </p>\n {method.isDefault && (\n <span className=\"px-1.5 py-0.5 text-xs font-medium bg-action-primary-bg/10 text-primary rounded\">\n Default\n </span>\n )}\n </div>\n {method.expiryMonth && method.expiryYear && (\n <p\n className={`text-sm ${isExpired ? 'text-status-error-text' : 'text-text-secondary'}`}\n >\n {isExpired ? 'Expired' : 'Expires'} {method.expiryMonth}/{method.expiryYear}\n </p>\n )}\n </div>\n </div>\n </div>\n\n {canManage && (\n <div className=\"flex items-center gap-2 mt-4 pt-4 border-t border-border-subtle\">\n {!method.isDefault && (\n <button\n type=\"button\"\n onClick={handleSetDefault}\n disabled={isSettingDefault || isDeleting}\n className=\"text-sm text-primary hover:underline disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isSettingDefault ? 'Setting default…' : 'Set as default'}\n </button>\n )}\n <button\n type=\"button\"\n onClick={handleDeleteRequest}\n disabled={isSettingDefault || isDeleting}\n className=\"text-sm text-status-error-text hover:underline ml-auto disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isDeleting ? 'Removing…' : 'Remove'}\n </button>\n </div>\n )}\n\n {isConfirmingDelete && (\n <div className=\"mt-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3\">\n <p className=\"text-sm font-medium text-status-error-text\">\n Remove this payment method from your billing account?\n </p>\n <div className=\"mt-3 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setIsConfirmingDelete(false)}\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={handleDelete}\n disabled={isDeleting}\n className=\"rounded-button bg-action-danger-bg px-3 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-danger-bgHover disabled:cursor-not-allowed disabled:opacity-50\"\n >\n {isDeleting ? 'Removing…' : 'Remove'}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { AccessDenied as e } from "../../../shared/components/AccessDenied.js";
|
|
2
|
+
import { PageHeader as t } from "../../../shared/components/PageHeader.js";
|
|
3
|
+
import { statusTokens as n } from "../../../shared/utils/tokens.js";
|
|
4
|
+
import { ServerError as r } from "../../../shared/components/ServerError.js";
|
|
5
|
+
import "../../../shared/components/index.js";
|
|
6
|
+
import { formatCurrency as i, formatDateTime as a } from "../../../shared/utils/format.js";
|
|
7
|
+
import { isServerError as o } from "../../../shared/utils/error.js";
|
|
8
|
+
import "../../../shared/utils/index.js";
|
|
9
|
+
import { useBillingPermissions as s } from "../../../hooks/useBillingPermissions.js";
|
|
10
|
+
import { useRefundRecords as c } from "../hooks/useRefunds.js";
|
|
11
|
+
import "../hooks/index.js";
|
|
12
|
+
import { useEffect as l, useMemo as u, useState as d } from "react";
|
|
13
|
+
import { AlertTriangle as f, ArrowLeft as p, RefreshCw as m, Search as h, X as g } from "lucide-react";
|
|
14
|
+
import { Link as _ } from "react-router";
|
|
15
|
+
import { jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
16
|
+
import { IllustratedEmptyState as b, ResponsiveTable as x } from "@burdenoff/fe-libs/ui";
|
|
17
|
+
//#region src/billing/modules/billing/pages/RefundIssueLogsPage.tsx
|
|
18
|
+
var S = 25, C = 350, w = () => {
|
|
19
|
+
let w = s(), [T, E] = d(""), [D, O] = d("");
|
|
20
|
+
l(() => {
|
|
21
|
+
let e = setTimeout(() => O(T.trim()), C);
|
|
22
|
+
return () => clearTimeout(e);
|
|
23
|
+
}, [T]);
|
|
24
|
+
let { records: k, pagination: A, isLoading: j, error: M, refetch: N, goToNextPage: P, goToPreviousPage: F } = c({
|
|
25
|
+
pageSize: S,
|
|
26
|
+
filters: u(() => ({
|
|
27
|
+
hasError: !0,
|
|
28
|
+
search: D || void 0
|
|
29
|
+
}), [D])
|
|
30
|
+
});
|
|
31
|
+
if (!w.canViewPayments) return /* @__PURE__ */ v(e, { message: "You don't have permission to view refund logs." });
|
|
32
|
+
if (M && o(M)) return /* @__PURE__ */ v("div", {
|
|
33
|
+
className: "p-6",
|
|
34
|
+
children: /* @__PURE__ */ v(r, {
|
|
35
|
+
title: "Server Unavailable",
|
|
36
|
+
message: "Unable to load refund issue logs. The server might be down or experiencing issues.",
|
|
37
|
+
onRetry: () => N(),
|
|
38
|
+
showRetry: !0
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
let I = [
|
|
42
|
+
{
|
|
43
|
+
key: "date",
|
|
44
|
+
header: "Date",
|
|
45
|
+
priority: "primary",
|
|
46
|
+
cellClassName: "whitespace-nowrap",
|
|
47
|
+
cell: (e) => /* @__PURE__ */ y("div", {
|
|
48
|
+
className: "flex flex-col",
|
|
49
|
+
children: [/* @__PURE__ */ v("span", {
|
|
50
|
+
className: "text-sm text-text-primary",
|
|
51
|
+
children: a(e.createdAt).split(" ")[0]
|
|
52
|
+
}), /* @__PURE__ */ v("span", {
|
|
53
|
+
className: "text-xs text-text-muted",
|
|
54
|
+
children: a(e.createdAt).split(" ")[1]
|
|
55
|
+
})]
|
|
56
|
+
})
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "status",
|
|
60
|
+
header: "Status",
|
|
61
|
+
cellClassName: "whitespace-nowrap",
|
|
62
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
63
|
+
className: `inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded ${n.success.bg} ${n.success.text}`,
|
|
64
|
+
children: e.status
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "provider",
|
|
69
|
+
header: "Provider",
|
|
70
|
+
priority: "hidden-on-mobile",
|
|
71
|
+
cellClassName: "whitespace-nowrap",
|
|
72
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
73
|
+
className: "text-sm text-text-primary capitalize",
|
|
74
|
+
children: e.provider
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "transactionId",
|
|
79
|
+
header: "Transaction ID",
|
|
80
|
+
priority: "hidden-on-mobile",
|
|
81
|
+
cellClassName: "whitespace-nowrap max-w-[140px] truncate",
|
|
82
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
83
|
+
className: "text-sm font-mono text-text-muted",
|
|
84
|
+
children: e.transactionId || "-"
|
|
85
|
+
})
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "paymentId",
|
|
89
|
+
header: "Payment ID",
|
|
90
|
+
cellClassName: "whitespace-nowrap max-w-[140px] truncate",
|
|
91
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
92
|
+
className: "text-sm font-mono text-text-muted",
|
|
93
|
+
children: e.gatewayPaymentId
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
key: "amount",
|
|
98
|
+
header: "Amount",
|
|
99
|
+
headerClassName: "text-right",
|
|
100
|
+
cellClassName: "text-right whitespace-nowrap",
|
|
101
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
102
|
+
className: "text-sm font-medium tabular-nums",
|
|
103
|
+
children: i(e.amount, e.currency)
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: "purchaseType",
|
|
108
|
+
header: "Type",
|
|
109
|
+
priority: "hidden-on-mobile",
|
|
110
|
+
cellClassName: "whitespace-nowrap",
|
|
111
|
+
cell: (e) => /* @__PURE__ */ v("span", {
|
|
112
|
+
className: "text-sm text-text-muted",
|
|
113
|
+
children: e.purchaseType || "-"
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "issue",
|
|
118
|
+
header: "Issue",
|
|
119
|
+
cellClassName: "max-w-[360px]",
|
|
120
|
+
cell: (e) => /* @__PURE__ */ y("div", {
|
|
121
|
+
className: "flex items-start gap-1.5 py-1",
|
|
122
|
+
children: [/* @__PURE__ */ v(f, { className: `size-3.5 mt-0.5 shrink-0 ${n.warning.icon}` }), /* @__PURE__ */ v("span", {
|
|
123
|
+
className: "text-sm text-status-warning-text",
|
|
124
|
+
children: e.errorMessage
|
|
125
|
+
})]
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
], L = Math.max(1, Math.ceil(A.totalCount / S));
|
|
129
|
+
return /* @__PURE__ */ y("div", {
|
|
130
|
+
className: "space-y-6 p-6",
|
|
131
|
+
children: [
|
|
132
|
+
/* @__PURE__ */ v(t, {
|
|
133
|
+
title: "Refund Issue Log",
|
|
134
|
+
description: "Refunds that succeeded with the payment gateway but left an accounting or reconciliation issue behind — e.g. a Zoho credit note that couldn't be recorded. The customer was refunded; these still need manual follow-up.",
|
|
135
|
+
actions: /* @__PURE__ */ y("div", {
|
|
136
|
+
className: "flex items-center gap-2",
|
|
137
|
+
children: [/* @__PURE__ */ y(_, {
|
|
138
|
+
to: "../refunds",
|
|
139
|
+
className: "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border-subtle bg-bg-surface hover:bg-bg-sunken/50",
|
|
140
|
+
children: [/* @__PURE__ */ v(p, { className: "size-4" }), "Back to Refunds"]
|
|
141
|
+
}), /* @__PURE__ */ y("button", {
|
|
142
|
+
type: "button",
|
|
143
|
+
onClick: () => N(),
|
|
144
|
+
className: "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border-subtle bg-bg-surface hover:bg-bg-sunken/50",
|
|
145
|
+
children: [/* @__PURE__ */ v(m, { className: "size-4" }), "Refresh"]
|
|
146
|
+
})]
|
|
147
|
+
})
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ y("div", {
|
|
150
|
+
className: "relative",
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ v(h, { className: "absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-muted" }),
|
|
153
|
+
/* @__PURE__ */ v("input", {
|
|
154
|
+
type: "text",
|
|
155
|
+
placeholder: "Search by transaction ID, refund ID, gateway payment ID, or gateway refund ID…",
|
|
156
|
+
value: T,
|
|
157
|
+
onChange: (e) => E(e.target.value),
|
|
158
|
+
className: "w-full pl-[3.25rem] pr-10 py-2 border border-border-subtle rounded-md bg-bg-surface"
|
|
159
|
+
}),
|
|
160
|
+
T && /* @__PURE__ */ v("button", {
|
|
161
|
+
type: "button",
|
|
162
|
+
onClick: () => E(""),
|
|
163
|
+
className: "absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bg-sunken/50",
|
|
164
|
+
"aria-label": "Clear search",
|
|
165
|
+
children: /* @__PURE__ */ v(g, { className: "size-4 text-text-muted" })
|
|
166
|
+
})
|
|
167
|
+
]
|
|
168
|
+
}),
|
|
169
|
+
j && k.length === 0 ? /* @__PURE__ */ v("div", {
|
|
170
|
+
className: "space-y-2",
|
|
171
|
+
children: [
|
|
172
|
+
1,
|
|
173
|
+
2,
|
|
174
|
+
3,
|
|
175
|
+
4,
|
|
176
|
+
5
|
|
177
|
+
].map((e) => /* @__PURE__ */ v("div", { className: "h-16 bg-bg-sunken animate-pulse rounded" }, e))
|
|
178
|
+
}) : k.length === 0 ? /* @__PURE__ */ v(b, {
|
|
179
|
+
illustration: D ? "empty-search" : "empty-data",
|
|
180
|
+
title: D ? "No matching refund issues" : "No open refund issues",
|
|
181
|
+
description: D ? `Nothing matched "${D}". Try the transaction ID, refund ID, or a payment/refund ID from the gateway.` : "Every completed refund reconciled cleanly — nothing here needs follow-up right now."
|
|
182
|
+
}) : /* @__PURE__ */ v("div", {
|
|
183
|
+
className: "border border-border-subtle rounded-lg overflow-hidden",
|
|
184
|
+
children: /* @__PURE__ */ v(x, {
|
|
185
|
+
columns: I,
|
|
186
|
+
data: k,
|
|
187
|
+
rowKey: (e) => e.id
|
|
188
|
+
})
|
|
189
|
+
}),
|
|
190
|
+
A.totalCount > 0 && /* @__PURE__ */ y("div", {
|
|
191
|
+
className: "flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-border-subtle pt-4",
|
|
192
|
+
children: [/* @__PURE__ */ y("p", {
|
|
193
|
+
className: "text-sm text-text-muted text-center sm:text-left",
|
|
194
|
+
children: [
|
|
195
|
+
"Showing ",
|
|
196
|
+
A.page * S + 1,
|
|
197
|
+
" to",
|
|
198
|
+
" ",
|
|
199
|
+
Math.min((A.page + 1) * S, A.totalCount),
|
|
200
|
+
" of",
|
|
201
|
+
" ",
|
|
202
|
+
A.totalCount,
|
|
203
|
+
" refunds with an open issue"
|
|
204
|
+
]
|
|
205
|
+
}), /* @__PURE__ */ y("div", {
|
|
206
|
+
className: "flex items-center gap-2",
|
|
207
|
+
children: [
|
|
208
|
+
/* @__PURE__ */ v("button", {
|
|
209
|
+
type: "button",
|
|
210
|
+
onClick: F,
|
|
211
|
+
disabled: !A.hasPrevious,
|
|
212
|
+
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/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
|
|
213
|
+
children: "Previous"
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ y("span", {
|
|
216
|
+
className: "text-sm text-text-muted px-2",
|
|
217
|
+
children: [
|
|
218
|
+
"Page ",
|
|
219
|
+
A.page + 1,
|
|
220
|
+
" of ",
|
|
221
|
+
L
|
|
222
|
+
]
|
|
223
|
+
}),
|
|
224
|
+
/* @__PURE__ */ v("button", {
|
|
225
|
+
type: "button",
|
|
226
|
+
onClick: P,
|
|
227
|
+
disabled: !A.hasMore,
|
|
228
|
+
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/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
|
|
229
|
+
children: "Next"
|
|
230
|
+
})
|
|
231
|
+
]
|
|
232
|
+
})]
|
|
233
|
+
})
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
//#endregion
|
|
238
|
+
export { w as RefundIssueLogsPage };
|
|
239
|
+
|
|
240
|
+
//# sourceMappingURL=RefundIssueLogsPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RefundIssueLogsPage.js","names":[],"sources":["../../../../../src/billing/modules/billing/pages/RefundIssueLogsPage.tsx"],"sourcesContent":["/**\n * Billing Module - Refund Issue Logs Page\n * Admin visibility into refunds that completed at the payment gateway but\n * carry an unresolved accounting/reconciliation issue (RefundRecord.errorMessage)\n * — e.g. a Zoho credit-note refund that couldn't be recorded because no bank\n * account is configured. These refunds are otherwise invisible: the customer\n * got their money back, so they don't show up as Failed, and the warning is\n * normally only visible by opening one refund's Details modal at a time.\n * This page surfaces every one of them in a single scannable list.\n */\n\nimport { useEffect, useMemo, useState, type FC } from 'react';\nimport { AlertTriangle, ArrowLeft, RefreshCw, Search, X } from 'lucide-react';\nimport { Link } from 'react-router';\nimport { useBillingPermissions } from '../../../hooks/useBillingPermissions';\nimport { useRefundRecords, type RefundRecord, type RefundRecordFilters } from '../hooks';\nimport { AccessDenied, PageHeader, ServerError } from '../../../shared/components';\nimport { formatCurrency, formatDateTime, isServerError } from '../../../shared/utils';\nimport { statusTokens } from '../../../shared/utils/tokens';\nimport {\n ResponsiveTable,\n type ResponsiveTableColumn,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\n\nconst PAGE_SIZE = 25;\nconst SEARCH_DEBOUNCE_MS = 350;\n\nexport const RefundIssueLogsPage: FC = () => {\n const permissions = useBillingPermissions();\n\n // Debounced so every keystroke doesn't fire a new server query.\n const [searchInput, setSearchInput] = useState('');\n const [debouncedSearch, setDebouncedSearch] = useState('');\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedSearch(searchInput.trim()), SEARCH_DEBOUNCE_MS);\n return () => clearTimeout(timer);\n }, [searchInput]);\n\n // Memoized so useRefundRecords' internal useEffect (keyed on options.filters\n // by reference) only re-fires when the debounced search actually changes,\n // not on every render — the same infinite-loop failure mode documented on\n // RefundsPage's tabFilters memo.\n const filters = useMemo<RefundRecordFilters>(\n () => ({ hasError: true, search: debouncedSearch || undefined }),\n [debouncedSearch]\n );\n\n const { records, pagination, isLoading, error, refetch, goToNextPage, goToPreviousPage } =\n useRefundRecords({\n pageSize: PAGE_SIZE,\n filters,\n });\n\n if (!permissions.canViewPayments) {\n return <AccessDenied message=\"You don't have permission to view refund logs.\" />;\n }\n\n if (error && isServerError(error)) {\n return (\n <div className=\"p-6\">\n <ServerError\n title=\"Server Unavailable\"\n message=\"Unable to load refund issue logs. The server might be down or experiencing issues.\"\n onRetry={() => refetch()}\n showRetry\n />\n </div>\n );\n }\n\n const columns: ResponsiveTableColumn<RefundRecord>[] = [\n {\n key: 'date',\n header: 'Date',\n priority: 'primary',\n cellClassName: 'whitespace-nowrap',\n cell: (record) => (\n <div className=\"flex flex-col\">\n <span className=\"text-sm text-text-primary\">\n {formatDateTime(record.createdAt).split(' ')[0]}\n </span>\n <span className=\"text-xs text-text-muted\">\n {formatDateTime(record.createdAt).split(' ')[1]}\n </span>\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n cellClassName: 'whitespace-nowrap',\n cell: (record) => (\n <span\n className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium rounded ${statusTokens.success.bg} ${statusTokens.success.text}`}\n >\n {record.status}\n </span>\n ),\n },\n {\n key: 'provider',\n header: 'Provider',\n priority: 'hidden-on-mobile',\n cellClassName: 'whitespace-nowrap',\n cell: (record) => (\n <span className=\"text-sm text-text-primary capitalize\">{record.provider}</span>\n ),\n },\n {\n key: 'transactionId',\n header: 'Transaction ID',\n priority: 'hidden-on-mobile',\n cellClassName: 'whitespace-nowrap max-w-[140px] truncate',\n cell: (record) => (\n <span className=\"text-sm font-mono text-text-muted\">{record.transactionId || '-'}</span>\n ),\n },\n {\n key: 'paymentId',\n header: 'Payment ID',\n cellClassName: 'whitespace-nowrap max-w-[140px] truncate',\n cell: (record) => (\n <span className=\"text-sm font-mono text-text-muted\">{record.gatewayPaymentId}</span>\n ),\n },\n {\n key: 'amount',\n header: 'Amount',\n headerClassName: 'text-right',\n cellClassName: 'text-right whitespace-nowrap',\n cell: (record) => (\n <span className=\"text-sm font-medium tabular-nums\">\n {formatCurrency(record.amount, record.currency)}\n </span>\n ),\n },\n {\n key: 'purchaseType',\n header: 'Type',\n priority: 'hidden-on-mobile',\n cellClassName: 'whitespace-nowrap',\n cell: (record) => (\n <span className=\"text-sm text-text-muted\">{record.purchaseType || '-'}</span>\n ),\n },\n {\n key: 'issue',\n header: 'Issue',\n cellClassName: 'max-w-[360px]',\n cell: (record) => (\n <div className=\"flex items-start gap-1.5 py-1\">\n <AlertTriangle className={`size-3.5 mt-0.5 shrink-0 ${statusTokens.warning.icon}`} />\n <span className=\"text-sm text-status-warning-text\">{record.errorMessage}</span>\n </div>\n ),\n },\n ];\n\n const totalPages = Math.max(1, Math.ceil(pagination.totalCount / PAGE_SIZE));\n\n return (\n <div className=\"space-y-6 p-6\">\n <PageHeader\n title=\"Refund Issue Log\"\n description=\"Refunds that succeeded with the payment gateway but left an accounting or reconciliation issue behind — e.g. a Zoho credit note that couldn't be recorded. The customer was refunded; these still need manual follow-up.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <Link\n to=\"../refunds\"\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border-subtle bg-bg-surface hover:bg-bg-sunken/50\"\n >\n <ArrowLeft className=\"size-4\" />\n Back to Refunds\n </Link>\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border-subtle bg-bg-surface hover:bg-bg-sunken/50\"\n >\n <RefreshCw className=\"size-4\" />\n Refresh\n </button>\n </div>\n }\n />\n\n {/* Search */}\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 by transaction ID, refund ID, gateway payment ID, or gateway refund ID…\"\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"w-full pl-[3.25rem] pr-10 py-2 border border-border-subtle rounded-md bg-bg-surface\"\n />\n {searchInput && (\n <button\n type=\"button\"\n onClick={() => setSearchInput('')}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded hover:bg-bg-sunken/50\"\n aria-label=\"Clear search\"\n >\n <X className=\"size-4 text-text-muted\" />\n </button>\n )}\n </div>\n\n {isLoading && records.length === 0 ? (\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 ) : records.length === 0 ? (\n <IllustratedEmptyState\n illustration={debouncedSearch ? 'empty-search' : 'empty-data'}\n title={debouncedSearch ? 'No matching refund issues' : 'No open refund issues'}\n description={\n debouncedSearch\n ? `Nothing matched \"${debouncedSearch}\". Try the transaction ID, refund ID, or a payment/refund ID from the gateway.`\n : 'Every completed refund reconciled cleanly — nothing here needs follow-up right now.'\n }\n />\n ) : (\n <div className=\"border border-border-subtle rounded-lg overflow-hidden\">\n <ResponsiveTable<RefundRecord>\n columns={columns}\n data={records}\n rowKey={(record) => record.id}\n />\n </div>\n )}\n\n {pagination.totalCount > 0 && (\n <div className=\"flex flex-col sm:flex-row items-center justify-between gap-4 border-t border-border-subtle pt-4\">\n <p className=\"text-sm text-text-muted text-center sm:text-left\">\n Showing {pagination.page * PAGE_SIZE + 1} to{' '}\n {Math.min((pagination.page + 1) * PAGE_SIZE, pagination.totalCount)} of{' '}\n {pagination.totalCount} refunds with an open issue\n </p>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={goToPreviousPage}\n disabled={!pagination.hasPrevious}\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/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Previous\n </button>\n <span className=\"text-sm text-text-muted px-2\">\n Page {pagination.page + 1} of {totalPages}\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/50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;AAyBA,IAAM,IAAY,IACZ,IAAqB,KAEd,UAAgC;CAC3C,IAAM,IAAc,EAAsB,GAGpC,CAAC,GAAa,KAAkB,EAAS,EAAE,GAC3C,CAAC,GAAiB,KAAsB,EAAS,EAAE;CACzD,QAAgB;EACd,IAAM,IAAQ,iBAAiB,EAAmB,EAAY,KAAK,CAAC,GAAG,CAAkB;EACzF,aAAa,aAAa,CAAK;CACjC,GAAG,CAAC,CAAW,CAAC;CAWhB,IAAM,EAAE,YAAS,eAAY,cAAW,UAAO,YAAS,iBAAc,wBACpE,EAAiB;EACf,UAAU;EACV,SARY,SACP;GAAE,UAAU;GAAM,QAAQ,KAAmB,KAAA;EAAU,IAC9D,CAAC,CAAe,CAMd;CACF,CAAC;CAEH,IAAI,CAAC,EAAY,iBACf,OAAO,kBAAC,GAAD,EAAc,SAAQ,iDAAkD,CAAA;CAGjF,IAAI,KAAS,EAAc,CAAK,GAC9B,OACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,OAAM;GACN,SAAQ;GACR,eAAe,EAAQ;GACvB,WAAA;EACD,CAAA;CACE,CAAA;CAIT,IAAM,IAAiD;EACrD;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,eAAe;GACf,OAAO,MACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAe,EAAO,SAAS,EAAE,MAAM,GAAG,EAAE;IACzC,CAAA,GACN,kBAAC,QAAD;KAAM,WAAU;eACb,EAAe,EAAO,SAAS,EAAE,MAAM,GAAG,EAAE;IACzC,CAAA,CACH;;EAET;EACA;GACE,KAAK;GACL,QAAQ;GACR,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IACE,WAAW,0EAA0E,EAAa,QAAQ,GAAG,GAAG,EAAa,QAAQ;cAEpI,EAAO;GACJ,CAAA;EAEV;EACA;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAwC,EAAO;GAAe,CAAA;EAElF;EACA;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAqC,EAAO,iBAAiB;GAAU,CAAA;EAE3F;EACA;GACE,KAAK;GACL,QAAQ;GACR,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAqC,EAAO;GAAuB,CAAA;EAEvF;EACA;GACE,KAAK;GACL,QAAQ;GACR,iBAAiB;GACjB,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cACb,EAAe,EAAO,QAAQ,EAAO,QAAQ;GAC1C,CAAA;EAEV;EACA;GACE,KAAK;GACL,QAAQ;GACR,UAAU;GACV,eAAe;GACf,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAA2B,EAAO,gBAAgB;GAAU,CAAA;EAEhF;EACA;GACE,KAAK;GACL,QAAQ;GACR,eAAe;GACf,OAAO,MACL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAe,WAAW,4BAA4B,EAAa,QAAQ,OAAS,CAAA,GACpF,kBAAC,QAAD;KAAM,WAAU;eAAoC,EAAO;IAAmB,CAAA,CAC3E;;EAET;CACF,GAEM,IAAa,KAAK,IAAI,GAAG,KAAK,KAAK,EAAW,aAAa,CAAS,CAAC;CAE3E,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,SACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,IAAG;MACH,WAAU;gBAFZ,CAIE,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA,GAAC,iBAE5B;SACN,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAQ;MACvB,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,SAAU,CAAA,GAAC,SAE1B;OACL;;GAER,CAAA;GAGD,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAQ,WAAU,kEAAmE,CAAA;KACrF,kBAAC,SAAD;MACE,MAAK;MACL,aAAY;MACZ,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAK;MAC9C,WAAU;KACX,CAAA;KACA,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAe,EAAE;MAChC,WAAU;MACV,cAAW;gBAEX,kBAAC,GAAD,EAAG,WAAU,yBAA0B,CAAA;KACjC,CAAA;IAEP;;GAEJ,KAAa,EAAQ,WAAW,IAC/B,kBAAC,OAAD;IAAK,WAAU;cACZ;KAAC;KAAG;KAAG;KAAG;KAAG;IAAC,EAAE,KAAK,MACpB,kBAAC,OAAD,EAAa,WAAU,0CAA2C,GAAxD,CAAwD,CACnE;GACE,CAAA,IACH,EAAQ,WAAW,IACrB,kBAAC,GAAD;IACE,cAAc,IAAkB,iBAAiB;IACjD,OAAO,IAAkB,8BAA8B;IACvD,aACE,IACI,oBAAoB,EAAgB,kFACpC;GAEP,CAAA,IAED,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACW;KACT,MAAM;KACN,SAAS,MAAW,EAAO;IAC5B,CAAA;GACE,CAAA;GAGN,EAAW,aAAa,KACvB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MAAgE;MACrD,EAAW,OAAO,IAAY;MAAE;MAAI;MAC5C,KAAK,KAAK,EAAW,OAAO,KAAK,GAAW,EAAW,UAAU;MAAE;MAAI;MACvE,EAAW;MAAW;KACtB;QACH,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU,CAAC,EAAW;OACtB,WAAU;iBACX;MAEO,CAAA;MACR,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QAA+C;QACvC,EAAW,OAAO;QAAE;QAAK;OAC3B;;MACN,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,UAAU,CAAC,EAAW;OACtB,WAAU;iBACX;MAEO,CAAA;KACL;MACF;;EAEJ;;AAET"}
|