@hanzo/ui 5.3.36 → 5.3.39
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/assets/crypto.d.ts.map +1 -1
- package/dist/assets/file.d.ts.map +1 -1
- package/dist/assets/general.d.ts.map +1 -1
- package/dist/assets/hanzo-logo.d.ts.map +1 -1
- package/dist/assets/llm-provider.d.ts.map +1 -1
- package/dist/components/cal-embed.d.ts.map +1 -1
- package/dist/frameworks/react/hooks/index.d.ts.map +1 -1
- package/dist/frameworks/react-native/utils.d.ts.map +1 -1
- package/dist/frameworks/svelte/utils.d.ts.map +1 -1
- package/dist/frameworks/vue/utils.d.ts.map +1 -1
- package/dist/helpers/file.d.ts.map +1 -1
- package/dist/helpers/memoization.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/dist/models/index.js +35 -0
- package/dist/models/index.mjs +35 -0
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/index.mjs +2 -1
- package/dist/primitives/alert.d.ts +1 -1
- package/dist/primitives/cal-embed.d.ts.map +1 -1
- package/dist/primitives/chart.d.ts.map +1 -1
- package/dist/primitives/chat/chat-input.d.ts.map +1 -1
- package/dist/primitives/combobox.d.ts.map +1 -1
- package/dist/primitives/command.d.ts.map +1 -1
- package/dist/primitives/copy-to-clipboard-icon.d.ts.map +1 -1
- package/dist/primitives/dots-loader.d.ts.map +1 -1
- package/dist/primitives/error-message.d.ts.map +1 -1
- package/dist/primitives/file-uploader.d.ts.map +1 -1
- package/dist/primitives/form.d.ts.map +1 -1
- package/dist/primitives/input-otp.d.ts.map +1 -1
- package/dist/primitives/markdown-preview.d.ts.map +1 -1
- package/dist/primitives/pretty-json-print.d.ts.map +1 -1
- package/dist/primitives/resizable.d.ts +4 -20
- package/dist/primitives/resizable.d.ts.map +1 -1
- package/dist/primitives/resizable.js +4 -4
- package/dist/primitives/sonner.d.ts.map +1 -1
- package/dist/primitives/text-link.d.ts.map +1 -1
- package/dist/primitives/textfield.d.ts.map +1 -1
- package/dist/primitives/toast.d.ts.map +1 -1
- package/dist/resizable.js +1 -1
- package/dist/resizable.mjs +1 -1
- package/dist/src/billing/components/account-members.d.ts +10 -0
- package/dist/src/billing/components/account-members.d.ts.map +1 -0
- package/dist/src/billing/components/account-members.js +63 -0
- package/dist/src/billing/components/account-switcher.d.ts +9 -0
- package/dist/src/billing/components/account-switcher.d.ts.map +1 -0
- package/dist/src/billing/components/account-switcher.js +52 -0
- package/dist/src/billing/components/animated-card.d.ts +9 -0
- package/dist/src/billing/components/animated-card.d.ts.map +1 -0
- package/dist/src/billing/components/animated-card.js +161 -0
- package/dist/src/billing/components/billing-settings.d.ts +26 -0
- package/dist/src/billing/components/billing-settings.d.ts.map +1 -0
- package/dist/src/billing/components/billing-settings.js +23 -0
- package/dist/src/billing/components/business-profile-panel.d.ts.map +1 -1
- package/dist/src/billing/components/business-profile-panel.js +9 -8
- package/dist/src/billing/components/card-form.d.ts +18 -0
- package/dist/src/billing/components/card-form.d.ts.map +1 -0
- package/dist/src/billing/components/card-form.js +139 -0
- package/dist/src/billing/components/cost-explorer.d.ts +7 -0
- package/dist/src/billing/components/cost-explorer.d.ts.map +1 -0
- package/dist/src/billing/components/cost-explorer.js +73 -0
- package/dist/src/billing/components/credits-panel.d.ts +8 -0
- package/dist/src/billing/components/credits-panel.d.ts.map +1 -0
- package/dist/src/billing/components/credits-panel.js +58 -0
- package/dist/src/billing/components/guided-setup.d.ts +13 -0
- package/dist/src/billing/components/guided-setup.d.ts.map +1 -0
- package/dist/src/billing/components/guided-setup.js +44 -0
- package/dist/src/billing/components/index.d.ts +34 -0
- package/dist/src/billing/components/index.d.ts.map +1 -1
- package/dist/src/billing/components/index.js +17 -0
- package/dist/src/billing/components/invoice-manager.js +13 -13
- package/dist/src/billing/components/invoices-payments.d.ts +7 -0
- package/dist/src/billing/components/invoices-payments.d.ts.map +1 -0
- package/dist/src/billing/components/invoices-payments.js +44 -0
- package/dist/src/billing/components/overview-dashboard.d.ts +31 -0
- package/dist/src/billing/components/overview-dashboard.d.ts.map +1 -0
- package/dist/src/billing/components/overview-dashboard.js +105 -0
- package/dist/src/billing/components/payment-manager.d.ts.map +1 -1
- package/dist/src/billing/components/payment-manager.js +267 -83
- package/dist/src/billing/components/promotions-panel.d.ts +7 -0
- package/dist/src/billing/components/promotions-panel.d.ts.map +1 -0
- package/dist/src/billing/components/promotions-panel.js +48 -0
- package/dist/src/billing/components/spend-alerts.d.ts +9 -0
- package/dist/src/billing/components/spend-alerts.d.ts.map +1 -0
- package/dist/src/billing/components/spend-alerts.js +99 -0
- package/dist/src/billing/components/square-card-form.d.ts +32 -0
- package/dist/src/billing/components/square-card-form.d.ts.map +1 -0
- package/dist/src/billing/components/square-card-form.js +179 -0
- package/dist/src/billing/components/status-bar.d.ts +12 -0
- package/dist/src/billing/components/status-bar.d.ts.map +1 -0
- package/dist/src/billing/components/status-bar.js +32 -0
- package/dist/src/billing/components/subscription-portal.d.ts +2 -1
- package/dist/src/billing/components/subscription-portal.d.ts.map +1 -1
- package/dist/src/billing/components/subscription-portal.js +123 -26
- package/dist/src/billing/components/support-tiers-panel.d.ts +6 -0
- package/dist/src/billing/components/support-tiers-panel.d.ts.map +1 -0
- package/dist/src/billing/components/support-tiers-panel.js +90 -0
- package/dist/src/billing/components/tax-compliance-panel.js +5 -5
- package/dist/src/billing/components/transactions-panel.d.ts +8 -0
- package/dist/src/billing/components/transactions-panel.d.ts.map +1 -0
- package/dist/src/billing/components/transactions-panel.js +64 -0
- package/dist/src/billing/components/usage-panel.d.ts +7 -0
- package/dist/src/billing/components/usage-panel.d.ts.map +1 -0
- package/dist/src/billing/components/usage-panel.js +64 -0
- package/dist/src/billing/types/index.d.ts +136 -1
- package/dist/src/billing/types/index.d.ts.map +1 -1
- package/dist/src/form/form.d.ts.map +1 -1
- package/dist/src/hooks/use-copy-clipboard.d.ts.map +1 -1
- package/dist/src/hooks/use-fill-ids.d.ts.map +1 -1
- package/dist/src/hooks/use-scroll-restoration.d.ts.map +1 -1
- package/dist/src/models/ModelCard.d.ts +25 -0
- package/dist/src/models/ModelCard.d.ts.map +1 -0
- package/dist/src/models/ModelCard.js +73 -0
- package/dist/src/models/ModelLibrary.d.ts +41 -0
- package/dist/src/models/ModelLibrary.d.ts.map +1 -0
- package/dist/src/models/ModelLibrary.js +63 -0
- package/dist/src/models/ModelTable.d.ts +17 -0
- package/dist/src/models/ModelTable.d.ts.map +1 -0
- package/dist/src/models/ModelTable.js +35 -0
- package/dist/src/models/ZenEnso.d.ts +17 -0
- package/dist/src/models/ZenEnso.d.ts.map +1 -0
- package/dist/src/models/ZenEnso.js +50 -0
- package/dist/src/models/index.d.ts +23 -0
- package/dist/src/models/index.d.ts.map +1 -0
- package/dist/src/models/index.js +18 -0
- package/dist/src/models/types.d.ts +40 -0
- package/dist/src/models/types.d.ts.map +1 -0
- package/dist/src/models/types.js +7 -0
- package/dist/src/navigation/hanzo-shell/AppSwitcher.d.ts +8 -0
- package/dist/src/navigation/hanzo-shell/AppSwitcher.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/AppSwitcher.js +19 -0
- package/dist/src/navigation/hanzo-shell/BeamAvatar.d.ts +9 -0
- package/dist/src/navigation/hanzo-shell/BeamAvatar.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/BeamAvatar.js +38 -0
- package/dist/src/navigation/hanzo-shell/HanzoCommandPalette.d.ts +28 -0
- package/dist/src/navigation/hanzo-shell/HanzoCommandPalette.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/HanzoCommandPalette.js +124 -0
- package/dist/src/navigation/hanzo-shell/HanzoHeader.d.ts +37 -0
- package/dist/src/navigation/hanzo-shell/HanzoHeader.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/HanzoHeader.js +43 -0
- package/dist/src/navigation/hanzo-shell/HanzoMark.d.ts +17 -0
- package/dist/src/navigation/hanzo-shell/HanzoMark.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/HanzoMark.js +57 -0
- package/dist/src/navigation/hanzo-shell/UserAvatar.d.ts +15 -0
- package/dist/src/navigation/hanzo-shell/UserAvatar.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/UserAvatar.js +44 -0
- package/dist/src/navigation/hanzo-shell/UserOrgDropdown.d.ts +11 -0
- package/dist/src/navigation/hanzo-shell/UserOrgDropdown.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/UserOrgDropdown.js +26 -0
- package/dist/src/navigation/hanzo-shell/index.d.ts +14 -0
- package/dist/src/navigation/hanzo-shell/index.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/index.js +9 -0
- package/dist/src/navigation/hanzo-shell/types.d.ts +49 -0
- package/dist/src/navigation/hanzo-shell/types.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/types.js +32 -0
- package/dist/src/navigation/hanzo-shell/useHanzoAuth.d.ts +16 -0
- package/dist/src/navigation/hanzo-shell/useHanzoAuth.d.ts.map +1 -0
- package/dist/src/navigation/hanzo-shell/useHanzoAuth.js +93 -0
- package/dist/src/navigation/index.d.ts +2 -0
- package/dist/src/navigation/index.d.ts.map +1 -1
- package/dist/src/navigation/index.js +2 -0
- package/dist/src/ui/banner.d.ts +1 -1
- package/dist/util/blob.d.ts.map +1 -1
- package/dist/util/blob.js +1 -1
- package/dist/util/date.d.ts.map +1 -1
- package/dist/util/debounce.d.ts.map +1 -1
- package/dist/util/file.d.ts.map +1 -1
- package/dist/util/format-and-abbreviate-as-currency.d.ts +7 -1
- package/dist/util/format-and-abbreviate-as-currency.d.ts.map +1 -1
- package/dist/util/format-text.d.ts.map +1 -1
- package/dist/util/format-to-max-char.d.ts +7 -1
- package/dist/util/format-to-max-char.d.ts.map +1 -1
- package/dist/util/index.mjs +1 -1
- package/dist/util/number-abbreviate.d.ts.map +1 -1
- package/dist/util/specifier.d.ts.map +1 -1
- package/dist/util/step-animation.d.ts.map +1 -1
- package/package.json +20 -7
- package/dist/tailwind/typo-plugin/get-plugin-styles.d.ts +0 -595
- package/dist/tailwind/typo-plugin/get-plugin-styles.d.ts.map +0 -1
- package/dist/tailwind/typo-plugin/get-plugin-styles.js +0 -661
- package/dist/tailwind/typo-plugin/index.d.ts +0 -3
- package/dist/tailwind/typo-plugin/index.d.ts.map +0 -1
- package/dist/tailwind/typo-plugin/index.js +0 -102
- package/dist/tailwind/typo-plugin/utils.d.ts +0 -6
- package/dist/tailwind/typo-plugin/utils.d.ts.map +0 -1
- package/dist/tailwind/typo-plugin/utils.js +0 -47
|
@@ -18,40 +18,53 @@ function formatDate(date) {
|
|
|
18
18
|
const statusTone = {
|
|
19
19
|
active: {
|
|
20
20
|
label: 'Active',
|
|
21
|
-
className: 'bg-emerald-
|
|
21
|
+
className: 'bg-emerald-500/10 text-emerald-500 ring-emerald-500/20',
|
|
22
22
|
},
|
|
23
23
|
canceled: {
|
|
24
24
|
label: 'Canceled',
|
|
25
|
-
className: 'bg-rose-
|
|
25
|
+
className: 'bg-rose-500/10 text-rose-500 ring-rose-500/20',
|
|
26
26
|
},
|
|
27
27
|
past_due: {
|
|
28
28
|
label: 'Past due',
|
|
29
|
-
className: 'bg-amber-
|
|
29
|
+
className: 'bg-amber-500/10 text-amber-500 ring-amber-500/20',
|
|
30
30
|
},
|
|
31
31
|
trialing: {
|
|
32
32
|
label: 'Trialing',
|
|
33
|
-
className: 'bg-sky-
|
|
33
|
+
className: 'bg-sky-500/10 text-sky-500 ring-sky-500/20',
|
|
34
34
|
},
|
|
35
35
|
unpaid: {
|
|
36
36
|
label: 'Unpaid',
|
|
37
|
-
className: 'bg-rose-
|
|
37
|
+
className: 'bg-rose-500/10 text-rose-500 ring-rose-500/20',
|
|
38
38
|
},
|
|
39
39
|
incomplete: {
|
|
40
40
|
label: 'Incomplete',
|
|
41
|
-
className: 'bg-
|
|
41
|
+
className: 'bg-text-dim/10 text-text-muted ring-text-dim/20',
|
|
42
42
|
},
|
|
43
43
|
};
|
|
44
44
|
export function SubscriptionPortal(props) {
|
|
45
|
-
const { subscription, availablePlans, usageMetrics = [], retentionOffers = [], subscriptionHistory = [], onUpgrade, onDowngrade, defaultView = 'overview', } = props;
|
|
45
|
+
const { subscription, availablePlans, usageMetrics = [], retentionOffers = [], subscriptionHistory = [], onUpgrade, onDowngrade, onCancel, onApplyDiscount, defaultView = 'overview', } = props;
|
|
46
46
|
const [view, setView] = React.useState(defaultView);
|
|
47
47
|
const [busyPlanId, setBusyPlanId] = React.useState(null);
|
|
48
|
+
// Discount code state
|
|
49
|
+
const [discountCode, setDiscountCode] = React.useState('');
|
|
50
|
+
const [applyingDiscount, setApplyingDiscount] = React.useState(false);
|
|
51
|
+
const [discountResult, setDiscountResult] = React.useState(null);
|
|
52
|
+
// Cancel confirmation
|
|
53
|
+
const [showCancelConfirm, setShowCancelConfirm] = React.useState(false);
|
|
54
|
+
const [cancelReason, setCancelReason] = React.useState('');
|
|
55
|
+
const [canceling, setCanceling] = React.useState(false);
|
|
48
56
|
const currentPlan = React.useMemo(() => availablePlans.find((p) => p.id === subscription.planId), [availablePlans, subscription.planId]);
|
|
49
57
|
const handlePlanChange = React.useCallback(async (plan) => {
|
|
50
|
-
|
|
58
|
+
// External / contact-sales plans
|
|
59
|
+
if (plan.externalUrl) {
|
|
60
|
+
window.open(plan.externalUrl, '_blank', 'noopener,noreferrer');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (plan.id === currentPlan?.id)
|
|
51
64
|
return;
|
|
52
65
|
setBusyPlanId(plan.id);
|
|
53
66
|
try {
|
|
54
|
-
if (plan.price >= currentPlan
|
|
67
|
+
if (!currentPlan || plan.price >= (currentPlan?.price ?? 0)) {
|
|
55
68
|
await onUpgrade?.(plan.id);
|
|
56
69
|
}
|
|
57
70
|
else {
|
|
@@ -62,27 +75,111 @@ export function SubscriptionPortal(props) {
|
|
|
62
75
|
setBusyPlanId(null);
|
|
63
76
|
}
|
|
64
77
|
}, [currentPlan, onDowngrade, onUpgrade]);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
const handleApplyDiscount = React.useCallback(async () => {
|
|
79
|
+
if (!onApplyDiscount || !discountCode.trim())
|
|
80
|
+
return;
|
|
81
|
+
setApplyingDiscount(true);
|
|
82
|
+
setDiscountResult(null);
|
|
83
|
+
try {
|
|
84
|
+
const result = await onApplyDiscount(discountCode.trim());
|
|
85
|
+
if (result && result.valid) {
|
|
86
|
+
const desc = result.kind === 'percent'
|
|
87
|
+
? `${result.value}% off`
|
|
88
|
+
: `${formatCurrency(result.value)} off`;
|
|
89
|
+
setDiscountResult({ success: true, message: `Applied: ${result.name || result.code} (${desc})` });
|
|
90
|
+
setDiscountCode('');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
setDiscountResult({ success: false, message: 'Invalid or expired discount code' });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
setDiscountResult({ success: false, message: err instanceof Error ? err.message : 'Failed to apply code' });
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setApplyingDiscount(false);
|
|
101
|
+
}
|
|
102
|
+
}, [onApplyDiscount, discountCode]);
|
|
103
|
+
const handleCancel = React.useCallback(async () => {
|
|
104
|
+
if (!onCancel || !cancelReason.trim())
|
|
105
|
+
return;
|
|
106
|
+
setCanceling(true);
|
|
107
|
+
try {
|
|
108
|
+
await onCancel(cancelReason.trim());
|
|
109
|
+
setShowCancelConfirm(false);
|
|
110
|
+
setCancelReason('');
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
setCanceling(false);
|
|
114
|
+
}
|
|
115
|
+
}, [onCancel, cancelReason]);
|
|
116
|
+
return (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { className: "overflow-hidden rounded-2xl border border-border bg-bg-card p-6", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-semibold uppercase tracking-[0.14em] text-text-muted", children: "Subscription" }), _jsx("h2", { className: "text-2xl font-semibold text-text", children: currentPlan?.name ?? 'No active plan' }), _jsx("p", { className: "text-sm text-text-secondary", children: subscription.id
|
|
117
|
+
? `Billing period ends on ${formatDate(subscription.currentPeriodEnd)}`
|
|
118
|
+
: 'Select a plan to get started' }), subscription.discount && (_jsxs("p", { className: "text-sm text-emerald-500", children: ["Discount: ", subscription.discount.name || subscription.discount.code, " (", subscription.discount.kind === 'percent'
|
|
119
|
+
? `${subscription.discount.value}% off`
|
|
120
|
+
: `${formatCurrency(subscription.discount.value)} off`, subscription.discount.duration === 'repeating' && subscription.discount.durationInMonths
|
|
121
|
+
? ` for ${subscription.discount.durationInMonths} months`
|
|
122
|
+
: subscription.discount.duration === 'forever'
|
|
123
|
+
? ' forever'
|
|
124
|
+
: '', ")"] }))] }), subscription.id && (_jsx("span", { className: `inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold ring-1 ${statusTone[subscription.status].className}`, children: statusTone[subscription.status].label }))] }), _jsxs("div", { className: "mt-6 flex flex-wrap gap-2 rounded-xl border border-border bg-bg-elevated p-1", children: [_jsx("button", { type: "button", onClick: () => setView('overview'), className: `rounded-lg px-3 py-1.5 text-sm transition ${view === 'overview'
|
|
125
|
+
? 'bg-text text-bg'
|
|
126
|
+
: 'text-text-muted hover:bg-bg-card hover:text-text'}`, children: "Overview" }), _jsx("button", { type: "button", onClick: () => setView('plans'), className: `rounded-lg px-3 py-1.5 text-sm transition ${view === 'plans'
|
|
127
|
+
? 'bg-text text-bg'
|
|
128
|
+
: 'text-text-muted hover:bg-bg-card hover:text-text'}`, children: "Plans" }), _jsx("button", { type: "button", onClick: () => setView('history'), className: `rounded-lg px-3 py-1.5 text-sm transition ${view === 'history'
|
|
129
|
+
? 'bg-text text-bg'
|
|
130
|
+
: 'text-text-muted hover:bg-bg-card hover:text-text'}`, children: "History" })] })] }), view === 'overview' && (_jsxs("div", { className: "grid gap-4 md:grid-cols-3", children: [_jsxs("div", { className: "rounded-xl border border-border bg-bg-card p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Current spend" }), _jsxs("p", { className: "mt-2 text-2xl font-semibold text-text", children: [formatCurrency(currentPlan?.price ?? 0), _jsxs("span", { className: "text-sm font-normal text-text-muted", children: ["/", currentPlan?.billingPeriod ?? 'monthly'] })] })] }), _jsxs("div", { className: "rounded-xl border border-border bg-bg-card p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Next invoice" }), _jsx("p", { className: "mt-2 text-lg font-semibold text-text", children: subscription.id ? formatDate(subscription.currentPeriodEnd) : '\u2014' })] }), _jsxs("div", { className: "rounded-xl border border-border bg-bg-card p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Status" }), _jsx("p", { className: "mt-2 text-lg font-semibold text-text", children: subscription.cancelAtPeriodEnd
|
|
131
|
+
? 'Cancels at period end'
|
|
132
|
+
: subscription.id
|
|
133
|
+
? 'Auto-renewing'
|
|
134
|
+
: 'No subscription' })] }), onApplyDiscount && subscription.id && (_jsxs("div", { className: "md:col-span-3 rounded-xl border border-border bg-bg-card p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Discount code" }), _jsxs("div", { className: "mt-3 flex gap-2", children: [_jsx("input", { value: discountCode, onChange: (e) => {
|
|
135
|
+
setDiscountCode(e.target.value.toUpperCase());
|
|
136
|
+
setDiscountResult(null);
|
|
137
|
+
}, placeholder: "Enter promo code", className: "flex-1 rounded-lg border border-border bg-bg-input px-3 py-2 text-sm font-mono uppercase text-text outline-none transition focus:border-brand" }), _jsx("button", { type: "button", disabled: !discountCode.trim() || applyingDiscount, onClick: handleApplyDiscount, className: "rounded-lg bg-text px-4 py-2 text-sm font-medium text-bg transition hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-60", children: applyingDiscount ? 'Applying\u2026' : 'Apply' })] }), discountResult && (_jsx("p", { className: `mt-2 text-sm ${discountResult.success ? 'text-success' : 'text-danger'}`, children: discountResult.message }))] })), usageMetrics.length > 0 && (_jsxs("div", { className: "md:col-span-3 rounded-xl border border-border bg-bg-card p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Usage metrics" }), _jsx("div", { className: "mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-4", children: usageMetrics.map((metric) => {
|
|
72
138
|
const utilization = metric.limit && metric.limit > 0
|
|
73
139
|
? Math.min((metric.current / metric.limit) * 100, 100)
|
|
74
140
|
: 0;
|
|
75
|
-
return (_jsxs("div", { className: "rounded-lg border border-
|
|
76
|
-
}) })] })), retentionOffers.length > 0 && (_jsxs("div", { className: "md:col-span-3 rounded-xl border border-
|
|
141
|
+
return (_jsxs("div", { className: "rounded-lg border border-border p-3", children: [_jsx("p", { className: "text-sm text-text-secondary", children: metric.label }), _jsxs("p", { className: "mt-1 text-lg font-semibold text-text", children: [metric.current.toLocaleString(), metric.unit ? ` ${metric.unit}` : ''] }), metric.limit ? (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["of ", metric.limit.toLocaleString(), " ", metric.unit ?? ''] }), _jsx("div", { className: "mt-2 h-2 rounded-full bg-bg-elevated", children: _jsx("div", { className: "h-2 rounded-full bg-brand transition-all", style: { width: `${utilization}%` } }) })] })) : null] }, metric.id));
|
|
142
|
+
}) })] })), retentionOffers.length > 0 && (_jsxs("div", { className: "md:col-span-3 rounded-xl border border-warning/30 bg-warning/5 p-4", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-warning", children: "Retention offers" }), _jsx("div", { className: "mt-3 grid gap-3 md:grid-cols-2", children: retentionOffers.map((offer) => (_jsxs("div", { className: "rounded-lg border border-warning/30 bg-bg-card p-3", children: [_jsx("p", { className: "font-medium text-text", children: offer.title }), _jsx("p", { className: "mt-1 text-sm text-text-secondary", children: offer.description }), _jsxs("p", { className: "mt-2 text-sm font-semibold text-warning", children: ["Save ", offer.discount, "% for ", offer.durationMonths, " months"] })] }, offer.id))) })] })), subscription.id && subscription.status === 'active' && !subscription.cancelAtPeriodEnd && onCancel && (_jsx("div", { className: "md:col-span-3", children: showCancelConfirm ? (_jsxs("div", { className: "rounded-xl border border-rose-500/30 bg-rose-500/5 p-4", children: [_jsx("p", { className: "text-sm font-medium text-rose-400", children: "Cancel subscription" }), _jsxs("p", { className: "mt-1 text-sm text-text-muted", children: ["Your subscription will remain active until ", formatDate(subscription.currentPeriodEnd), "."] }), _jsx("textarea", { value: cancelReason, onChange: (e) => setCancelReason(e.target.value), placeholder: "Why are you canceling? (required)", rows: 2, className: "mt-3 w-full rounded-lg border border-rose-500/30 bg-bg-input px-3 py-2 text-sm text-text outline-none transition focus:border-rose-500" }), _jsxs("div", { className: "mt-3 flex gap-2", children: [_jsx("button", { type: "button", disabled: !cancelReason.trim() || canceling, onClick: handleCancel, className: "rounded-lg bg-rose-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-rose-500 disabled:cursor-not-allowed disabled:opacity-60", children: canceling ? 'Canceling\u2026' : 'Confirm cancellation' }), _jsx("button", { type: "button", onClick: () => {
|
|
143
|
+
setShowCancelConfirm(false);
|
|
144
|
+
setCancelReason('');
|
|
145
|
+
}, className: "rounded-lg border border-border px-4 py-2 text-sm font-medium text-text-secondary transition hover:bg-bg-elevated", children: "Keep subscription" })] })] })) : (_jsx("button", { type: "button", onClick: () => setShowCancelConfirm(true), className: "text-sm text-text-muted transition hover:text-rose-500", children: "Cancel subscription" })) }))] })), view === 'plans' && (_jsx("div", { className: "grid gap-4 md:grid-cols-2 xl:grid-cols-4", children: availablePlans.map((plan) => {
|
|
77
146
|
const isCurrent = plan.id === subscription.planId;
|
|
78
147
|
const isBusy = busyPlanId === plan.id;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
148
|
+
const isFree = plan.price === 0;
|
|
149
|
+
const hasTrial = (plan.trialDays ?? 0) > 0;
|
|
150
|
+
const isExternal = !!plan.externalUrl || plan.contactSales;
|
|
151
|
+
const periodPrice = isFree ? 'Free' : `$${plan.price}/mo`;
|
|
152
|
+
const buttonLabel = isCurrent
|
|
153
|
+
? 'Current plan'
|
|
154
|
+
: isBusy
|
|
155
|
+
? 'Applying\u2026'
|
|
156
|
+
: isExternal
|
|
157
|
+
? plan.contactSales ? 'Contact Sales \u2192' : 'Get started \u2192'
|
|
158
|
+
: hasTrial && !subscription.id
|
|
159
|
+
? `Start ${plan.trialDays}-day free trial`
|
|
160
|
+
: currentPlan && plan.price < (currentPlan?.price ?? 0)
|
|
161
|
+
? 'Downgrade'
|
|
162
|
+
: 'Upgrade';
|
|
163
|
+
return (_jsxs("div", { className: `relative flex flex-col rounded-2xl border p-5 transition ${plan.highlighted && !isCurrent
|
|
164
|
+
? 'border-white/[0.2] bg-[#141419]'
|
|
165
|
+
: isCurrent
|
|
166
|
+
? 'border-white/[0.25] bg-[#141419]'
|
|
167
|
+
: 'border-white/[0.08] bg-[#141419]'}`, children: [plan.highlighted && !isCurrent && (_jsx("div", { className: "absolute -top-px left-0 right-0 h-px rounded-t-2xl bg-gradient-to-r from-transparent via-white/40 to-transparent" })), _jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsx("p", { className: "text-[15px] font-semibold text-white", children: plan.name }), plan.badge ? (_jsx("span", { className: `rounded-full px-2 py-1 text-[11px] font-semibold flex-shrink-0 ${isCurrent
|
|
168
|
+
? 'bg-white/10 text-white/60'
|
|
169
|
+
: plan.contactSales
|
|
170
|
+
? 'bg-amber-500/15 text-amber-400'
|
|
171
|
+
: hasTrial
|
|
172
|
+
? 'bg-sky-500/15 text-sky-400'
|
|
173
|
+
: 'bg-white/[0.07] text-white/40'}`, children: plan.badge })) : null] }), _jsxs("div", { className: "mt-4", children: [_jsxs("p", { className: "text-2xl font-bold text-white", children: [isExternal && !isFree ? `$${formatCurrency(plan.price).replace('$', '').split('.')[0]}` : periodPrice, !isFree && !isExternal && _jsx("span", { className: "text-sm font-normal text-white/40", children: "/mo" }), isExternal && !isFree && _jsx("span", { className: "text-sm font-normal text-white/40", children: "/mo+" })] }), plan.annualPrice && !isCurrent && (_jsxs("p", { className: "mt-0.5 text-[12px] text-emerald-400", children: ["$", plan.annualPrice, "/yr ", '\u2014', " save $", plan.price * 12 - plan.annualPrice] })), (plan.trialCreditCents ?? 0) > 0 && !isCurrent && (_jsxs("p", { className: "mt-0.5 text-[12px] text-emerald-400", children: ["+$", (plan.trialCreditCents / 100).toFixed(0), " credit included"] }))] }), _jsx("p", { className: "mt-3 text-[13px] leading-relaxed text-white/40", children: plan.description }), _jsx("ul", { className: "mt-4 flex-1 space-y-1.5", children: plan.features.map((feature) => (_jsxs("li", { className: "flex items-start gap-2 text-[13px] text-white/50", children: [_jsx("span", { className: "mt-0.5 flex-shrink-0 text-white/30", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M20 6L9 17l-5-5" }) }) }), feature] }, feature))) }), _jsx("button", { type: "button", disabled: isCurrent || isBusy, onClick: () => handlePlanChange(plan), className: `mt-5 w-full rounded-xl px-3 py-2.5 text-sm font-semibold transition active:scale-[0.98] ${isCurrent
|
|
174
|
+
? 'cursor-not-allowed bg-white/[0.06] text-white/30'
|
|
84
175
|
: isBusy
|
|
85
|
-
? 'cursor-wait bg-
|
|
86
|
-
:
|
|
87
|
-
|
|
176
|
+
? 'cursor-wait bg-white/[0.06] text-white/30'
|
|
177
|
+
: plan.highlighted
|
|
178
|
+
? 'bg-white text-black hover:bg-white/90'
|
|
179
|
+
: isExternal
|
|
180
|
+
? 'bg-white/[0.08] text-white/70 hover:bg-white/[0.12] hover:text-white'
|
|
181
|
+
: hasTrial && !subscription.id
|
|
182
|
+
? 'bg-sky-500/20 text-sky-300 hover:bg-sky-500/30'
|
|
183
|
+
: 'bg-white/[0.08] text-white/70 hover:bg-white/[0.12] hover:text-white'}`, children: buttonLabel })] }, plan.id));
|
|
184
|
+
}) })), view === 'history' && (_jsxs("div", { className: "rounded-xl border border-border bg-bg-card", children: [_jsx("div", { className: "border-b border-border px-4 py-3", children: _jsx("h3", { className: "text-sm font-semibold text-text", children: "Subscription timeline" }) }), subscriptionHistory.length === 0 ? (_jsx("div", { className: "p-6 text-sm text-text-muted", children: "No history entries yet." })) : (_jsx("div", { className: "divide-y divide-border", children: subscriptionHistory.map((entry) => (_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-2 px-4 py-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-text", children: entry.action }), _jsx("p", { className: "text-sm text-text-secondary", children: entry.details }), entry.fromPlan || entry.toPlan ? (_jsxs("p", { className: "text-xs text-text-muted", children: [entry.fromPlan ?? '\u2014', " ", '\u2192', " ", entry.toPlan ?? '\u2014'] })) : null] }), _jsx("p", { className: "text-xs text-text-muted", children: formatDate(entry.date) })] }, entry.id))) }))] }))] }));
|
|
88
185
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface SupportTiersPanelProps {
|
|
2
|
+
currentTier?: string;
|
|
3
|
+
onSubscribe?: (tierId: string) => Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export declare function SupportTiersPanel({ currentTier, onSubscribe }: SupportTiersPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=support-tiers-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"support-tiers-panel.d.ts","sourceRoot":"","sources":["../../../../src/billing/components/support-tiers-panel.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,sBAAsB;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAsDD,wBAAgB,iBAAiB,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,EAAE,sBAAsB,2CA6FrF"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
const SUPPORT_TIERS = [
|
|
5
|
+
{
|
|
6
|
+
id: 'community',
|
|
7
|
+
name: 'Community',
|
|
8
|
+
price: 0,
|
|
9
|
+
billingPeriod: 'monthly',
|
|
10
|
+
features: [
|
|
11
|
+
'Community forum access',
|
|
12
|
+
'Public documentation',
|
|
13
|
+
'GitHub issues',
|
|
14
|
+
'72-hour response SLA',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'priority',
|
|
19
|
+
name: 'Priority',
|
|
20
|
+
price: 99,
|
|
21
|
+
billingPeriod: 'monthly',
|
|
22
|
+
highlighted: true,
|
|
23
|
+
features: [
|
|
24
|
+
'Email support',
|
|
25
|
+
'Dedicated support channel',
|
|
26
|
+
'24-hour response SLA',
|
|
27
|
+
'Priority bug fixes',
|
|
28
|
+
'Quarterly architecture review',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'enterprise',
|
|
33
|
+
name: 'Enterprise',
|
|
34
|
+
price: 499,
|
|
35
|
+
billingPeriod: 'monthly',
|
|
36
|
+
features: [
|
|
37
|
+
'Dedicated support engineer',
|
|
38
|
+
'Slack/Teams integration',
|
|
39
|
+
'4-hour response SLA',
|
|
40
|
+
'Custom SLAs available',
|
|
41
|
+
'Onboarding assistance',
|
|
42
|
+
'Monthly strategy calls',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
function formatCurrency(value) {
|
|
47
|
+
if (value === 0)
|
|
48
|
+
return 'Free';
|
|
49
|
+
return new Intl.NumberFormat('en-US', {
|
|
50
|
+
style: 'currency',
|
|
51
|
+
currency: 'USD',
|
|
52
|
+
maximumFractionDigits: 0,
|
|
53
|
+
}).format(value);
|
|
54
|
+
}
|
|
55
|
+
export function SupportTiersPanel({ currentTier, onSubscribe }) {
|
|
56
|
+
const [busyTierId, setBusyTierId] = React.useState(null);
|
|
57
|
+
const handleSubscribe = React.useCallback(async (tierId) => {
|
|
58
|
+
if (!onSubscribe)
|
|
59
|
+
return;
|
|
60
|
+
setBusyTierId(tierId);
|
|
61
|
+
try {
|
|
62
|
+
await onSubscribe(tierId);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
setBusyTierId(null);
|
|
66
|
+
}
|
|
67
|
+
}, [onSubscribe]);
|
|
68
|
+
const activeTier = currentTier ?? 'community';
|
|
69
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card", children: _jsxs("div", { className: "border-b border-border p-4", children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Support plans" }), _jsx("p", { className: "text-sm text-text-muted", children: "Choose the level of support that fits your needs. Upgrade anytime." })] }) }), _jsx("div", { className: "grid gap-4 md:grid-cols-3", children: SUPPORT_TIERS.map((tier) => {
|
|
70
|
+
const isCurrent = tier.id === activeTier;
|
|
71
|
+
const isBusy = busyTierId === tier.id;
|
|
72
|
+
return (_jsxs("div", { className: `relative rounded-xl border p-5 transition ${tier.highlighted && !isCurrent
|
|
73
|
+
? 'border-2 border-brand bg-bg-card'
|
|
74
|
+
: isCurrent
|
|
75
|
+
? 'border-brand bg-brand/10'
|
|
76
|
+
: 'border-border bg-bg-card'}`, children: [tier.highlighted && !isCurrent && (_jsx("span", { className: "absolute -top-3 left-4 rounded-full bg-brand px-3 py-0.5 text-xs font-semibold text-brand-foreground", children: "Recommended" })), _jsx("p", { className: "text-lg font-semibold text-text", children: tier.name }), _jsxs("p", { className: "mt-2 text-2xl font-bold text-text", children: [formatCurrency(tier.price), tier.price > 0 && (_jsxs("span", { className: "text-sm font-normal text-text-muted", children: ["/", tier.billingPeriod] }))] }), _jsx("ul", { className: "mt-4 space-y-2 text-sm text-text-secondary", children: tier.features.map((feature) => (_jsxs("li", { className: "flex items-start gap-2", children: [_jsx("span", { className: "mt-0.5 text-emerald-500", children: '\u2713' }), feature] }, feature))) }), _jsx("button", { type: "button", disabled: isCurrent || isBusy || !onSubscribe, onClick: () => handleSubscribe(tier.id), className: `mt-5 w-full rounded-lg px-3 py-2 text-sm font-medium transition ${isCurrent
|
|
77
|
+
? 'cursor-not-allowed bg-bg-elevated text-text-dim'
|
|
78
|
+
: isBusy
|
|
79
|
+
? 'cursor-wait bg-bg-elevated text-text-muted'
|
|
80
|
+
: tier.highlighted
|
|
81
|
+
? 'bg-brand text-brand-foreground hover:bg-brand-hover active:scale-[0.97]'
|
|
82
|
+
: 'bg-text text-bg hover:opacity-80'}`, children: isCurrent
|
|
83
|
+
? 'Current plan'
|
|
84
|
+
: isBusy
|
|
85
|
+
? 'Subscribing...'
|
|
86
|
+
: tier.price === 0
|
|
87
|
+
? 'Downgrade'
|
|
88
|
+
: 'Upgrade' })] }, tier.id));
|
|
89
|
+
}) })] }));
|
|
90
|
+
}
|
|
@@ -4,19 +4,19 @@ import * as React from 'react';
|
|
|
4
4
|
const complianceTone = {
|
|
5
5
|
complete: {
|
|
6
6
|
label: 'Complete',
|
|
7
|
-
className: 'bg-emerald-
|
|
7
|
+
className: 'bg-emerald-500/10 text-emerald-500 ring-emerald-500/20',
|
|
8
8
|
},
|
|
9
9
|
required: {
|
|
10
10
|
label: 'Required',
|
|
11
|
-
className: 'bg-rose-
|
|
11
|
+
className: 'bg-rose-500/10 text-rose-500 ring-rose-500/20',
|
|
12
12
|
},
|
|
13
13
|
review: {
|
|
14
14
|
label: 'Review',
|
|
15
|
-
className: 'bg-amber-
|
|
15
|
+
className: 'bg-amber-500/10 text-amber-500 ring-amber-500/20',
|
|
16
16
|
},
|
|
17
17
|
blocked: {
|
|
18
18
|
label: 'Blocked',
|
|
19
|
-
className: 'bg-
|
|
19
|
+
className: 'bg-text-dim/10 text-text-muted ring-text-dim/20',
|
|
20
20
|
},
|
|
21
21
|
};
|
|
22
22
|
export function TaxCompliancePanel(props) {
|
|
@@ -37,5 +37,5 @@ export function TaxCompliancePanel(props) {
|
|
|
37
37
|
setSaving(false);
|
|
38
38
|
}
|
|
39
39
|
}, [automaticTax, onToggleAutomaticTax]);
|
|
40
|
-
return (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "grid gap-4 xl:grid-cols-5", children: [_jsxs("div", { className: "xl:col-span-2 overflow-hidden rounded-xl border border-
|
|
40
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "grid gap-4 xl:grid-cols-5", children: [_jsxs("div", { className: "xl:col-span-2 overflow-hidden rounded-xl border border-border bg-bg-card", children: [_jsxs("div", { className: "border-b border-border p-4", children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Tax settings" }), _jsx("p", { className: "text-sm text-text-muted", children: "Manage registrations, nexus, and tax collection behavior" })] }), _jsxs("div", { className: "space-y-4 p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-4 rounded-lg border border-border p-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-text", children: "Automatic tax" }), _jsx("p", { className: "text-xs text-text-muted", children: "Calculate and collect tax where required" })] }), _jsx("button", { type: "button", onClick: handleToggle, disabled: saving, className: `relative inline-flex h-6 w-11 items-center rounded-full transition ${automaticTax ? 'bg-brand' : 'bg-border-strong'}`, children: _jsx("span", { className: `inline-block h-4 w-4 transform rounded-full bg-white transition ${automaticTax ? 'translate-x-6' : 'translate-x-1'}` }) })] }), _jsxs("div", { children: [_jsx("p", { className: "mb-2 text-sm font-medium text-text", children: "Registrations" }), _jsx("div", { className: "space-y-2", children: taxSettings.registrations.length === 0 ? (_jsx("p", { className: "rounded-lg border border-dashed border-border p-3 text-sm text-text-muted", children: "No tax registrations configured." })) : (taxSettings.registrations.map((registration) => (_jsxs("div", { className: "rounded-lg border border-border p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "text-sm font-medium text-text", children: registration.region }), _jsx("span", { className: "rounded-full bg-bg-elevated px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-text-secondary", children: registration.status })] }), _jsxs("p", { className: "mt-1 text-xs text-text-muted", children: [registration.type.toUpperCase(), " ", '\u00b7', " ", registration.registrationId] })] }, registration.id)))) })] })] })] }), _jsxs("div", { className: "xl:col-span-3 overflow-hidden rounded-xl border border-border bg-bg-card", children: [_jsxs("div", { className: "border-b border-border p-4", children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Compliance checklist" }), _jsx("p", { className: "text-sm text-text-muted", children: "Track KYC/KYB, tax filings, and policy requirements" })] }), _jsx("div", { className: "divide-y divide-border", children: complianceItems.length === 0 ? (_jsx("div", { className: "p-6 text-sm text-text-muted", children: "No compliance tasks yet." })) : (complianceItems.map((item) => (_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 p-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-sm font-medium text-text", children: item.title }), _jsx("p", { className: "mt-1 text-sm text-text-secondary", children: item.description }), _jsxs("p", { className: "mt-1 text-xs text-text-muted", children: ["Category: ", item.category.toUpperCase(), item.owner ? ` \u00b7 Owner: ${item.owner}` : ''] })] }), _jsxs("div", { className: "flex flex-col items-end gap-2", children: [_jsx("span", { className: `inline-flex rounded-full px-2 py-1 text-xs font-semibold ring-1 ${complianceTone[item.status].className}`, children: complianceTone[item.status].label }), item.actionLabel ? (_jsx("button", { type: "button", className: "rounded-md border border-border px-3 py-1.5 text-xs font-medium text-text-secondary transition hover:bg-bg-elevated", children: item.actionLabel })) : null] })] }, item.id)))) })] })] }) }));
|
|
41
41
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TransactionRecord } from '../types';
|
|
2
|
+
export interface TransactionsPanelProps {
|
|
3
|
+
transactions: TransactionRecord[];
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
pageSize?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function TransactionsPanel({ transactions, loading, pageSize }: TransactionsPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=transactions-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transactions-panel.d.ts","sourceRoot":"","sources":["../../../../src/billing/components/transactions-panel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAmB,MAAM,UAAU,CAAA;AAElE,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,iBAAiB,EAAE,CAAA;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAqCD,wBAAgB,iBAAiB,CAAC,EAAE,YAAY,EAAE,OAAe,EAAE,QAAa,EAAE,EAAE,sBAAsB,2CAqIzG"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
function formatCurrency(value, currency = 'USD') {
|
|
5
|
+
return new Intl.NumberFormat('en-US', {
|
|
6
|
+
style: 'currency',
|
|
7
|
+
currency,
|
|
8
|
+
minimumFractionDigits: 2,
|
|
9
|
+
}).format(value);
|
|
10
|
+
}
|
|
11
|
+
function formatDate(dateStr) {
|
|
12
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
13
|
+
month: 'short',
|
|
14
|
+
day: 'numeric',
|
|
15
|
+
year: 'numeric',
|
|
16
|
+
hour: 'numeric',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
}).format(new Date(dateStr));
|
|
19
|
+
}
|
|
20
|
+
const TYPE_LABELS = {
|
|
21
|
+
deposit: { label: 'Deposit', color: 'text-emerald-500' },
|
|
22
|
+
withdraw: { label: 'Withdrawal', color: 'text-rose-500' },
|
|
23
|
+
hold: { label: 'Hold', color: 'text-amber-500' },
|
|
24
|
+
'hold-removed': { label: 'Hold Released', color: 'text-sky-500' },
|
|
25
|
+
transfer: { label: 'Transfer', color: 'text-violet-500' },
|
|
26
|
+
};
|
|
27
|
+
const typeFilterOptions = [
|
|
28
|
+
'all',
|
|
29
|
+
'deposit',
|
|
30
|
+
'withdraw',
|
|
31
|
+
'hold',
|
|
32
|
+
'hold-removed',
|
|
33
|
+
'transfer',
|
|
34
|
+
];
|
|
35
|
+
export function TransactionsPanel({ transactions, loading = false, pageSize = 20 }) {
|
|
36
|
+
const [typeFilter, setTypeFilter] = React.useState('all');
|
|
37
|
+
const [page, setPage] = React.useState(1);
|
|
38
|
+
const filtered = React.useMemo(() => {
|
|
39
|
+
if (typeFilter === 'all')
|
|
40
|
+
return transactions;
|
|
41
|
+
return transactions.filter((t) => t.type === typeFilter);
|
|
42
|
+
}, [transactions, typeFilter]);
|
|
43
|
+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
44
|
+
const pageRows = React.useMemo(() => {
|
|
45
|
+
const safePage = Math.min(page, totalPages);
|
|
46
|
+
const start = (safePage - 1) * pageSize;
|
|
47
|
+
return filtered.slice(start, start + pageSize);
|
|
48
|
+
}, [filtered, page, pageSize, totalPages]);
|
|
49
|
+
React.useEffect(() => {
|
|
50
|
+
if (page > totalPages)
|
|
51
|
+
setPage(totalPages);
|
|
52
|
+
}, [page, totalPages]);
|
|
53
|
+
if (loading) {
|
|
54
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card p-8 text-center", children: [_jsx("div", { className: "mx-auto h-6 w-6 animate-spin rounded-full border-2 border-text-dim border-t-brand" }), _jsx("p", { className: "mt-3 text-sm text-text-muted", children: "Loading transactions..." })] }));
|
|
55
|
+
}
|
|
56
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3 border-b border-border p-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Transactions" }), _jsxs("p", { className: "text-sm text-text-muted", children: [filtered.length, " transaction(s)"] })] }), _jsx("select", { value: typeFilter, onChange: (e) => {
|
|
57
|
+
setTypeFilter(e.target.value);
|
|
58
|
+
setPage(1);
|
|
59
|
+
}, className: "rounded-lg border border-border bg-bg-input px-3 py-2 text-sm text-text-secondary outline-none transition focus:border-brand", children: typeFilterOptions.map((t) => (_jsx("option", { value: t, children: t === 'all' ? 'All types' : TYPE_LABELS[t].label }, t))) })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[640px]", children: [_jsx("thead", { className: "bg-bg-elevated", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-muted", children: "Date" }), _jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-muted", children: "Type" }), _jsx("th", { className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-text-muted", children: "Amount" }), _jsx("th", { className: "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-text-muted", children: "Description" }), _jsx("th", { className: "px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-text-muted", children: "Balance" })] }) }), _jsx("tbody", { className: "divide-y divide-border", children: pageRows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: 5, className: "px-4 py-10 text-center text-sm text-text-muted", children: "No transactions found." }) })) : (pageRows.map((tx) => {
|
|
60
|
+
const typeInfo = TYPE_LABELS[tx.type] ?? { label: tx.type, color: 'text-text-muted' };
|
|
61
|
+
const isPositive = tx.type === 'deposit' || tx.type === 'hold-removed';
|
|
62
|
+
return (_jsxs("tr", { children: [_jsx("td", { className: "px-4 py-3 text-sm text-text-secondary", children: formatDate(tx.createdAt) }), _jsx("td", { className: "px-4 py-3 text-sm", children: _jsx("span", { className: `font-medium ${typeInfo.color}`, children: typeInfo.label }) }), _jsxs("td", { className: `px-4 py-3 text-right text-sm font-medium ${isPositive ? 'text-emerald-500' : 'text-rose-500'}`, children: [isPositive ? '+' : '-', formatCurrency(Math.abs(tx.amountCents) / 100, tx.currency)] }), _jsx("td", { className: "px-4 py-3 text-sm text-text-muted", children: tx.description || '\u2014' }), _jsx("td", { className: "px-4 py-3 text-right text-sm text-text-secondary", children: tx.balanceAfter != null ? formatCurrency(tx.balanceAfter / 100, tx.currency) : '\u2014' })] }, tx.id));
|
|
63
|
+
})) })] }) }), totalPages > 1 && (_jsxs("div", { className: "flex items-center justify-between border-t border-border p-4", children: [_jsxs("p", { className: "text-xs text-text-muted", children: ["Page ", Math.min(page, totalPages), " of ", totalPages] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", onClick: () => setPage((p) => Math.max(1, p - 1)), disabled: page <= 1, className: "rounded-md border border-border px-3 py-1.5 text-xs font-medium text-text-secondary transition hover:bg-bg-elevated disabled:cursor-not-allowed disabled:opacity-60", children: "Previous" }), _jsx("button", { type: "button", onClick: () => setPage((p) => Math.min(totalPages, p + 1)), disabled: page >= totalPages, className: "rounded-md border border-border px-3 py-1.5 text-xs font-medium text-text-secondary transition hover:bg-bg-elevated disabled:cursor-not-allowed disabled:opacity-60", children: "Next" })] })] }))] }) }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UsageSummary } from '../types';
|
|
2
|
+
export interface UsagePanelProps {
|
|
3
|
+
usage: UsageSummary | null;
|
|
4
|
+
loading?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function UsagePanel(props: UsagePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=usage-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-panel.d.ts","sourceRoot":"","sources":["../../../../src/billing/components/usage-panel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,UAAU,CAAA;AAEzD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAwGD,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,2CAoDhD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
function formatCurrency(value, currency = 'USD') {
|
|
4
|
+
return new Intl.NumberFormat('en-US', {
|
|
5
|
+
style: 'currency',
|
|
6
|
+
currency,
|
|
7
|
+
minimumFractionDigits: 2,
|
|
8
|
+
maximumFractionDigits: 2,
|
|
9
|
+
}).format(value);
|
|
10
|
+
}
|
|
11
|
+
function formatNumber(value) {
|
|
12
|
+
if (value >= 1000000)
|
|
13
|
+
return `${(value / 1000000).toFixed(1)}M`;
|
|
14
|
+
if (value >= 1000)
|
|
15
|
+
return `${(value / 1000).toFixed(1)}k`;
|
|
16
|
+
return value.toLocaleString();
|
|
17
|
+
}
|
|
18
|
+
function formatDate(dateStr) {
|
|
19
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
20
|
+
month: 'short',
|
|
21
|
+
day: 'numeric',
|
|
22
|
+
}).format(new Date(dateStr));
|
|
23
|
+
}
|
|
24
|
+
const METER_ICONS = {
|
|
25
|
+
ai: '\u{1F916}',
|
|
26
|
+
storage: '\u{1F4BE}',
|
|
27
|
+
network: '\u{1F310}',
|
|
28
|
+
network_egress: '\u{1F4E4}',
|
|
29
|
+
gpu: '\u26A1',
|
|
30
|
+
api_calls: '\u{1F4E1}',
|
|
31
|
+
};
|
|
32
|
+
const METER_COLORS = {
|
|
33
|
+
ai: 'bg-violet-500',
|
|
34
|
+
storage: 'bg-blue-500',
|
|
35
|
+
network: 'bg-emerald-500',
|
|
36
|
+
network_egress: 'bg-teal-500',
|
|
37
|
+
gpu: 'bg-amber-500',
|
|
38
|
+
api_calls: 'bg-indigo-500',
|
|
39
|
+
};
|
|
40
|
+
function UsageMeter({ record }) {
|
|
41
|
+
const utilization = record.limit && record.limit > 0
|
|
42
|
+
? Math.min((record.current / record.limit) * 100, 100)
|
|
43
|
+
: null;
|
|
44
|
+
const icon = METER_ICONS[record.meterId] ?? '\u{1F4CA}';
|
|
45
|
+
const barColor = METER_COLORS[record.meterId] ?? 'bg-brand';
|
|
46
|
+
const isOverage = utilization !== null && utilization >= 100;
|
|
47
|
+
const isWarning = utilization !== null && utilization >= 80 && utilization < 100;
|
|
48
|
+
return (_jsxs("div", { className: "rounded-lg border border-border bg-bg-card p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-lg", children: icon }), _jsx("p", { className: "text-sm font-medium text-text", children: record.label })] }), record.cost > 0 && (_jsx("span", { className: "rounded-full bg-bg-elevated px-2 py-0.5 text-xs font-medium text-text-secondary", children: formatCurrency(record.cost) }))] }), _jsxs("div", { className: "mt-3", children: [_jsxs("div", { className: "flex items-end justify-between", children: [_jsxs("p", { className: "text-2xl font-semibold text-text", children: [formatNumber(record.current), _jsx("span", { className: "ml-1 text-sm font-normal text-text-muted", children: record.unit })] }), record.limit && record.limit > 0 && (_jsxs("p", { className: "text-xs text-text-muted", children: ["of ", formatNumber(record.limit), " ", record.unit] }))] }), utilization !== null && (_jsxs("div", { className: "mt-2", children: [_jsx("div", { className: "h-2 rounded-full bg-bg-elevated", children: _jsx("div", { className: `h-2 rounded-full transition-all ${isOverage ? 'bg-danger' : isWarning ? 'bg-warning' : barColor}`, style: { width: `${Math.min(utilization, 100)}%` } }) }), _jsx("p", { className: `mt-1 text-xs ${isOverage ? 'text-danger font-medium' : isWarning ? 'text-warning' : 'text-text-muted'}`, children: isOverage
|
|
49
|
+
? `${(utilization - 100).toFixed(0)}% over limit`
|
|
50
|
+
: `${utilization.toFixed(0)}% used` })] }))] })] }));
|
|
51
|
+
}
|
|
52
|
+
export function UsagePanel(props) {
|
|
53
|
+
const { usage, loading = false } = props;
|
|
54
|
+
if (loading) {
|
|
55
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card p-8 text-center", children: [_jsx("div", { className: "mx-auto h-6 w-6 animate-spin rounded-full border-2 border-text-dim border-t-brand" }), _jsx("p", { className: "mt-3 text-sm text-text-muted", children: "Loading usage data..." })] }));
|
|
56
|
+
}
|
|
57
|
+
if (!usage || usage.records.length === 0) {
|
|
58
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card", children: [_jsxs("div", { className: "border-b border-border p-4", children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Usage" }), _jsx("p", { className: "text-sm text-text-muted", children: "Current billing period resource consumption" })] }), _jsx("div", { className: "p-6 text-sm text-text-muted", children: "No usage data available for this period." })] }));
|
|
59
|
+
}
|
|
60
|
+
const periodLabel = usage.period.start && usage.period.end
|
|
61
|
+
? `${formatDate(usage.period.start)} \u2013 ${formatDate(usage.period.end)}`
|
|
62
|
+
: 'Current period';
|
|
63
|
+
return (_jsx("div", { className: "space-y-4", children: _jsxs("div", { className: "overflow-hidden rounded-xl border border-border bg-bg-card", children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3 border-b border-border p-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold text-text", children: "Usage" }), _jsx("p", { className: "text-sm text-text-muted", children: periodLabel })] }), _jsxs("div", { className: "text-right", children: [_jsx("p", { className: "text-xs uppercase tracking-wide text-text-muted", children: "Total cost" }), _jsx("p", { className: "mt-1 text-xl font-semibold text-text", children: formatCurrency(usage.totalCost, usage.currency) })] })] }), _jsx("div", { className: "grid gap-4 p-4 md:grid-cols-2 xl:grid-cols-3", children: usage.records.map((record) => (_jsx(UsageMeter, { record: record }, record.meterId))) })] }) }));
|
|
64
|
+
}
|