@umituz/web-dashboard 2.1.1 → 2.3.0
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/package.json +24 -1
- package/src/domains/analytics/components/AnalyticsCard.tsx +80 -0
- package/src/domains/analytics/components/AnalyticsChart.tsx +184 -0
- package/src/domains/analytics/components/AnalyticsLayout.tsx +150 -0
- package/src/domains/analytics/components/MetricCard.tsx +108 -0
- package/src/domains/analytics/components/index.ts +10 -0
- package/src/domains/analytics/hooks/index.ts +7 -0
- package/src/domains/analytics/hooks/useAnalytics.ts +178 -0
- package/src/domains/analytics/index.ts +62 -0
- package/src/domains/analytics/types/analytics.ts +291 -0
- package/src/domains/analytics/types/index.ts +26 -0
- package/src/domains/analytics/utils/analytics.ts +333 -0
- package/src/domains/analytics/utils/index.ts +26 -0
- package/src/domains/billing/components/BillingLayout.tsx +63 -0
- package/src/domains/billing/components/BillingPortal.tsx +198 -0
- package/src/domains/billing/components/InvoiceCard.tsx +143 -0
- package/src/domains/billing/components/PaymentMethodsList.tsx +116 -0
- package/src/domains/billing/components/PlanComparison.tsx +199 -0
- package/src/domains/billing/components/UsageCard.tsx +103 -0
- package/src/domains/billing/components/index.ts +12 -0
- package/src/domains/billing/hooks/index.ts +7 -0
- package/src/domains/billing/hooks/useBilling.ts +385 -0
- package/src/domains/billing/index.ts +69 -0
- package/src/domains/billing/types/billing.ts +347 -0
- package/src/domains/billing/types/index.ts +28 -0
- package/src/domains/billing/utils/billing.ts +344 -0
- package/src/domains/billing/utils/index.ts +29 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for analytics operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Metric, KPIData, DateRangePreset } from "../types/analytics";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format number with K/M/B suffixes
|
|
11
|
+
*
|
|
12
|
+
* @param num - Number to format
|
|
13
|
+
* @param decimals - Number of decimal places (default: 1)
|
|
14
|
+
* @returns Formatted string
|
|
15
|
+
*/
|
|
16
|
+
export function formatNumber(num: number, decimals: number = 1): string {
|
|
17
|
+
if (num >= 1_000_000_000) {
|
|
18
|
+
return (num / 1_000_000_000).toFixed(decimals) + "B";
|
|
19
|
+
}
|
|
20
|
+
if (num >= 1_000_000) {
|
|
21
|
+
return (num / 1_000_000).toFixed(decimals) + "M";
|
|
22
|
+
}
|
|
23
|
+
if (num >= 1_000) {
|
|
24
|
+
return (num / 1_000).toFixed(decimals) + "K";
|
|
25
|
+
}
|
|
26
|
+
return num.toFixed(decimals);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format percentage
|
|
31
|
+
*
|
|
32
|
+
* @param value - Value to format as percentage
|
|
33
|
+
* @param decimals - Number of decimal places (default: 1)
|
|
34
|
+
* @returns Formatted string with % suffix
|
|
35
|
+
*/
|
|
36
|
+
export function formatPercentage(value: number, decimals: number = 1): string {
|
|
37
|
+
return `${value.toFixed(decimals)}%`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format currency
|
|
42
|
+
*
|
|
43
|
+
* @param value - Value to format
|
|
44
|
+
* @param currency - Currency code (default: USD)
|
|
45
|
+
* @param decimals - Number of decimal places (default: 0)
|
|
46
|
+
* @returns Formatted currency string
|
|
47
|
+
*/
|
|
48
|
+
export function formatCurrency(
|
|
49
|
+
value: number,
|
|
50
|
+
currency: string = "USD",
|
|
51
|
+
decimals: number = 0
|
|
52
|
+
): string {
|
|
53
|
+
return new Intl.NumberFormat("en-US", {
|
|
54
|
+
style: "currency",
|
|
55
|
+
currency,
|
|
56
|
+
minimumFractionDigits: decimals,
|
|
57
|
+
maximumFractionDigits: decimals,
|
|
58
|
+
}).format(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate growth rate
|
|
63
|
+
*
|
|
64
|
+
* @param current - Current value
|
|
65
|
+
* @param previous - Previous value
|
|
66
|
+
* @returns Growth rate percentage
|
|
67
|
+
*/
|
|
68
|
+
export function calculateGrowth(current: number, previous: number): number {
|
|
69
|
+
if (previous === 0) return current > 0 ? 100 : 0;
|
|
70
|
+
return ((current - previous) / previous) * 100;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get trend direction from growth
|
|
75
|
+
*
|
|
76
|
+
* @param growth - Growth rate
|
|
77
|
+
* @returns Trend direction
|
|
78
|
+
*/
|
|
79
|
+
export function getTrend(growth: number): "up" | "down" | "stable" {
|
|
80
|
+
if (growth > 0.1) return "up";
|
|
81
|
+
if (growth < -0.1) return "down";
|
|
82
|
+
return "stable";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create KPI data object
|
|
87
|
+
*
|
|
88
|
+
* @param current - Current value
|
|
89
|
+
* @param previous - Previous value
|
|
90
|
+
* @returns KPI data object
|
|
91
|
+
*/
|
|
92
|
+
export function createKPI(current: number, previous: number): KPIData {
|
|
93
|
+
const growth = calculateGrowth(current, previous);
|
|
94
|
+
return {
|
|
95
|
+
current,
|
|
96
|
+
previous,
|
|
97
|
+
growth,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create metric object
|
|
103
|
+
*
|
|
104
|
+
* @param id - Metric ID
|
|
105
|
+
* @param name - Metric name
|
|
106
|
+
* @param value - Current value
|
|
107
|
+
* @param previousValue - Previous value
|
|
108
|
+
* @param unit - Optional unit
|
|
109
|
+
* @returns Metric object
|
|
110
|
+
*/
|
|
111
|
+
export function createMetric(
|
|
112
|
+
id: string,
|
|
113
|
+
name: string,
|
|
114
|
+
value: number,
|
|
115
|
+
previousValue?: number,
|
|
116
|
+
unit?: string
|
|
117
|
+
): Metric {
|
|
118
|
+
const growth = previousValue !== undefined ? calculateGrowth(value, previousValue) : 0;
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
name,
|
|
122
|
+
value,
|
|
123
|
+
previousValue,
|
|
124
|
+
unit,
|
|
125
|
+
trend: getTrend(growth),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Format metric value based on unit
|
|
131
|
+
*
|
|
132
|
+
* @param metric - Metric object
|
|
133
|
+
* @returns Formatted value string
|
|
134
|
+
*/
|
|
135
|
+
export function formatMetricValue(metric: Metric): string {
|
|
136
|
+
const { value, unit } = metric;
|
|
137
|
+
|
|
138
|
+
switch (unit) {
|
|
139
|
+
case "%":
|
|
140
|
+
return formatPercentage(value);
|
|
141
|
+
case "$":
|
|
142
|
+
case "€":
|
|
143
|
+
case "£":
|
|
144
|
+
return formatCurrency(value, unit === "$" ? "USD" : unit === "€" ? "EUR" : "GBP");
|
|
145
|
+
case "K":
|
|
146
|
+
case "M":
|
|
147
|
+
case "B":
|
|
148
|
+
return formatNumber(value);
|
|
149
|
+
default:
|
|
150
|
+
return value.toLocaleString();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Calculate conversion rate
|
|
156
|
+
*
|
|
157
|
+
* @param converted - Number of conversions
|
|
158
|
+
* @param total - Total number of users
|
|
159
|
+
* @returns Conversion rate percentage
|
|
160
|
+
*/
|
|
161
|
+
export function calculateConversionRate(converted: number, total: number): number {
|
|
162
|
+
if (total === 0) return 0;
|
|
163
|
+
return (converted / total) * 100;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Calculate drop-off rate
|
|
168
|
+
*
|
|
169
|
+
* @param current - Current step count
|
|
170
|
+
* @param previous - Previous step count
|
|
171
|
+
* @returns Drop-off rate percentage
|
|
172
|
+
*/
|
|
173
|
+
export function calculateDropOffRate(current: number, previous: number): number {
|
|
174
|
+
if (previous === 0) return 0;
|
|
175
|
+
return ((previous - current) / previous) * 100;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Generate date range preset
|
|
180
|
+
*
|
|
181
|
+
* @param label - Preset label
|
|
182
|
+
* @param days - Number of days
|
|
183
|
+
* @returns Date range preset
|
|
184
|
+
*/
|
|
185
|
+
export function createDateRangePreset(label: string, days: number): DateRangePreset {
|
|
186
|
+
const to = new Date();
|
|
187
|
+
const from = new Date();
|
|
188
|
+
from.setDate(from.getDate() - days);
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
label,
|
|
192
|
+
value: label.toLowerCase().replace(/\s+/g, "-"),
|
|
193
|
+
days,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get common date range presets
|
|
199
|
+
*
|
|
200
|
+
* @returns Array of date range presets
|
|
201
|
+
*/
|
|
202
|
+
export function getDateRangePresets(): DateRangePreset[] {
|
|
203
|
+
return [
|
|
204
|
+
createDateRangePreset("Last 7 Days", 7),
|
|
205
|
+
createDateRangePreset("Last 30 Days", 30),
|
|
206
|
+
createDateRangePreset("Last 90 Days", 90),
|
|
207
|
+
createDateRangePreset("This Year", 365),
|
|
208
|
+
createDateRangePreset("All Time", 365 * 10),
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Aggregate data by time period
|
|
214
|
+
*
|
|
215
|
+
* @param data - Array of data points with date field
|
|
216
|
+
* @param period - Aggregation period (day, week, month)
|
|
217
|
+
* @returns Aggregated data
|
|
218
|
+
*/
|
|
219
|
+
export function aggregateByPeriod(
|
|
220
|
+
data: Array<{ date: string; [key: string]: number | string }>,
|
|
221
|
+
period: "day" | "week" | "month" = "day"
|
|
222
|
+
): Array<{ date: string; [key: string]: number }> {
|
|
223
|
+
const grouped = new Map<string, Array<typeof data[0]>>();
|
|
224
|
+
|
|
225
|
+
data.forEach((item) => {
|
|
226
|
+
const date = new Date(item.date);
|
|
227
|
+
let key: string;
|
|
228
|
+
|
|
229
|
+
if (period === "day") {
|
|
230
|
+
key = date.toISOString().split("T")[0];
|
|
231
|
+
} else if (period === "week") {
|
|
232
|
+
const weekStart = new Date(date);
|
|
233
|
+
weekStart.setDate(date.getDate() - date.getDay());
|
|
234
|
+
key = weekStart.toISOString().split("T")[0];
|
|
235
|
+
} else {
|
|
236
|
+
// month
|
|
237
|
+
key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!grouped.has(key)) {
|
|
241
|
+
grouped.set(key, []);
|
|
242
|
+
}
|
|
243
|
+
grouped.get(key)!.push(item);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return Array.from(grouped.entries()).map(([date, items]) => {
|
|
247
|
+
const aggregated: any = { date };
|
|
248
|
+
|
|
249
|
+
// Sum all numeric fields
|
|
250
|
+
items.forEach((item) => {
|
|
251
|
+
Object.entries(item).forEach(([key, value]) => {
|
|
252
|
+
if (key !== "date" && typeof value === "number") {
|
|
253
|
+
aggregated[key] = (aggregated[key] || 0) + value;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return aggregated;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Calculate moving average
|
|
264
|
+
*
|
|
265
|
+
* @param data - Array of numbers
|
|
266
|
+
* @param window - Window size
|
|
267
|
+
* @returns Array of moving averages
|
|
268
|
+
*/
|
|
269
|
+
export function calculateMovingAverage(data: number[], window: number): number[] {
|
|
270
|
+
const result: number[] = [];
|
|
271
|
+
|
|
272
|
+
for (let i = 0; i < data.length; i++) {
|
|
273
|
+
const start = Math.max(0, i - window + 1);
|
|
274
|
+
const subset = data.slice(start, i + 1);
|
|
275
|
+
const avg = subset.reduce((sum, val) => sum + val, 0) / subset.length;
|
|
276
|
+
result.push(avg);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Detect outliers in data
|
|
284
|
+
*
|
|
285
|
+
* @param data - Array of numbers
|
|
286
|
+
* @param threshold - Standard deviation threshold (default: 2)
|
|
287
|
+
* @returns Array of outlier indices
|
|
288
|
+
*/
|
|
289
|
+
export function detectOutliers(data: number[], threshold: number = 2): number[] {
|
|
290
|
+
const mean = data.reduce((sum, val) => sum + val, 0) / data.length;
|
|
291
|
+
const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
|
|
292
|
+
const stdDev = Math.sqrt(variance);
|
|
293
|
+
|
|
294
|
+
return data
|
|
295
|
+
.map((value, index) => ({ value, index }))
|
|
296
|
+
.filter(({ value }) => Math.abs(value - mean) > threshold * stdDev)
|
|
297
|
+
.map(({ index }) => index);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Round to decimal places
|
|
302
|
+
*
|
|
303
|
+
* @param num - Number to round
|
|
304
|
+
* @param decimals - Decimal places (default: 2)
|
|
305
|
+
* @returns Rounded number
|
|
306
|
+
*/
|
|
307
|
+
export function roundTo(num: number, decimals: number = 2): number {
|
|
308
|
+
const multiplier = Math.pow(10, decimals);
|
|
309
|
+
return Math.round(num * multiplier) / multiplier;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate random color
|
|
314
|
+
*
|
|
315
|
+
* @param index - Color index
|
|
316
|
+
* @param alpha - Alpha value (0-1)
|
|
317
|
+
* @returns HSL color string
|
|
318
|
+
*/
|
|
319
|
+
export function generateColor(index: number, alpha: number = 1): string {
|
|
320
|
+
const hue = (index * 137.508) % 360; // Golden angle approximation
|
|
321
|
+
return `hsla(${hue}, 70%, 50%, ${alpha})`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Generate chart colors
|
|
326
|
+
*
|
|
327
|
+
* @param count - Number of colors
|
|
328
|
+
* @param alpha - Alpha value (0-1)
|
|
329
|
+
* @returns Array of color strings
|
|
330
|
+
*/
|
|
331
|
+
export function generateChartColors(count: number, alpha: number = 1): string[] {
|
|
332
|
+
return Array.from({ length: count }, (_, i) => generateColor(i, alpha));
|
|
333
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Utilities
|
|
3
|
+
*
|
|
4
|
+
* Export all analytics utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
formatNumber,
|
|
9
|
+
formatPercentage,
|
|
10
|
+
formatCurrency,
|
|
11
|
+
calculateGrowth,
|
|
12
|
+
getTrend,
|
|
13
|
+
createKPI,
|
|
14
|
+
createMetric,
|
|
15
|
+
formatMetricValue,
|
|
16
|
+
calculateConversionRate,
|
|
17
|
+
calculateDropOffRate,
|
|
18
|
+
createDateRangePreset,
|
|
19
|
+
getDateRangePresets,
|
|
20
|
+
aggregateByPeriod,
|
|
21
|
+
calculateMovingAverage,
|
|
22
|
+
detectOutliers,
|
|
23
|
+
roundTo,
|
|
24
|
+
generateColor,
|
|
25
|
+
generateChartColors,
|
|
26
|
+
} from "./analytics";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Layout Component
|
|
3
|
+
*
|
|
4
|
+
* Layout wrapper for billing pages
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { BrandLogo } from "../../layouts/components";
|
|
8
|
+
import type { BillingLayoutProps } from "../types/billing";
|
|
9
|
+
|
|
10
|
+
export const BillingLayout = ({ config, children }: BillingLayoutProps) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="min-h-screen bg-background flex flex-col">
|
|
13
|
+
{/* Header */}
|
|
14
|
+
<header className="border-b border-border px-6 py-4 flex items-center justify-between">
|
|
15
|
+
<div className="flex items-center gap-2">
|
|
16
|
+
<BrandLogo size={28} />
|
|
17
|
+
<span className="font-bold text-xl text-foreground">{config.brandName}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<a
|
|
20
|
+
href="/dashboard"
|
|
21
|
+
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
22
|
+
>
|
|
23
|
+
Back to Dashboard
|
|
24
|
+
</a>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
{/* Main Content */}
|
|
28
|
+
<main className="flex-1 container max-w-6xl mx-auto px-4 py-12">
|
|
29
|
+
{/* Page Title */}
|
|
30
|
+
<div className="mb-8">
|
|
31
|
+
<h1 className="text-3xl font-bold text-foreground mb-2">
|
|
32
|
+
Billing & Subscription
|
|
33
|
+
</h1>
|
|
34
|
+
<p className="text-muted-foreground">
|
|
35
|
+
Manage your subscription, payment methods, and invoices
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
{/* Children Content */}
|
|
40
|
+
{children}
|
|
41
|
+
</main>
|
|
42
|
+
|
|
43
|
+
{/* Support Link */}
|
|
44
|
+
{config.supportEmail && (
|
|
45
|
+
<footer className="border-t border-border px-6 py-4">
|
|
46
|
+
<div className="container max-w-6xl mx-auto text-center">
|
|
47
|
+
<p className="text-sm text-muted-foreground">
|
|
48
|
+
Need help? Contact{" "}
|
|
49
|
+
<a
|
|
50
|
+
href={`mailto:${config.supportEmail}`}
|
|
51
|
+
className="text-primary hover:underline"
|
|
52
|
+
>
|
|
53
|
+
{config.supportEmail}
|
|
54
|
+
</a>
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
</footer>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default BillingLayout;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Portal Component
|
|
3
|
+
*
|
|
4
|
+
* Main billing portal with tabs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CreditCard, FileText, BarChart3, Settings, Loader2, AlertCircle } from "lucide-react";
|
|
8
|
+
import { cn } from "@umituz/web-design-system/utils";
|
|
9
|
+
import { Button } from "@umituz/web-design-system/atoms";
|
|
10
|
+
import type { BillingPortalProps } from "../types/billing";
|
|
11
|
+
import { UsageCard } from "./UsageCard";
|
|
12
|
+
import { PaymentMethodsList } from "./PaymentMethodsList";
|
|
13
|
+
import { InvoiceCard } from "./InvoiceCard";
|
|
14
|
+
import { getDaysRemaining, getStatusColor, getStatusLabel, formatPrice } from "../utils/billing";
|
|
15
|
+
|
|
16
|
+
export const BillingPortal = ({
|
|
17
|
+
billing,
|
|
18
|
+
loading = false,
|
|
19
|
+
error,
|
|
20
|
+
showTabs = true,
|
|
21
|
+
activeTab = "overview",
|
|
22
|
+
onTabChange,
|
|
23
|
+
}: BillingPortalProps) => {
|
|
24
|
+
const tabs = [
|
|
25
|
+
{ id: "overview", label: "Overview", icon: BarChart3 },
|
|
26
|
+
{ id: "payment-methods", label: "Payment Methods", icon: CreditCard },
|
|
27
|
+
{ id: "invoices", label: "Invoices", icon: FileText },
|
|
28
|
+
{ id: "plan", label: "Plan", icon: Settings },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (loading) {
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex items-center justify-center py-24">
|
|
34
|
+
<Loader2 className="h-12 w-12 animate-spin text-muted-foreground" />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex items-center justify-center py-24 gap-4 text-destructive">
|
|
42
|
+
<AlertCircle className="h-6 w-6" />
|
|
43
|
+
<p>{error}</p>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!billing) return null;
|
|
49
|
+
|
|
50
|
+
const renderContent = () => {
|
|
51
|
+
switch (activeTab) {
|
|
52
|
+
case "overview":
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
{/* Current Subscription */}
|
|
56
|
+
<div className="p-6 rounded-xl border border-border bg-background">
|
|
57
|
+
<h3 className="text-lg font-semibold text-foreground mb-4">
|
|
58
|
+
Current Subscription
|
|
59
|
+
</h3>
|
|
60
|
+
<div className="flex items-start justify-between">
|
|
61
|
+
<div>
|
|
62
|
+
<p className="text-2xl font-bold text-foreground">
|
|
63
|
+
{billing.subscription.plan.name}
|
|
64
|
+
</p>
|
|
65
|
+
<p className={cn("text-sm font-medium", getStatusColor(billing.subscription.status))}>
|
|
66
|
+
{getStatusLabel(billing.subscription.status)}
|
|
67
|
+
</p>
|
|
68
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
69
|
+
{formatPrice(
|
|
70
|
+
getPlanPrice(billing.subscription.plan, billing.subscription.cycle),
|
|
71
|
+
billing.subscription.plan.currency
|
|
72
|
+
)}
|
|
73
|
+
/{billing.subscription.cycle}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="text-right">
|
|
77
|
+
{billing.subscription.status === "trialing" && billing.subscription.trialEnd && (
|
|
78
|
+
<p className="text-sm text-muted-foreground">
|
|
79
|
+
{getDaysRemaining(billing.subscription.trialEnd)} days left in trial
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
{billing.upcomingInvoice && (
|
|
83
|
+
<p className="text-sm text-muted-foreground">
|
|
84
|
+
Next billing:{" "}
|
|
85
|
+
{new Date(billing.upcomingInvoice.date).toLocaleDateString()}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Usage Metrics */}
|
|
93
|
+
{billing.usage.length > 0 && (
|
|
94
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
95
|
+
{billing.usage.map((metric) => (
|
|
96
|
+
<UsageCard key={metric.id} metric={metric} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
{/* Upcoming Invoice */}
|
|
102
|
+
{billing.upcomingInvoice && (
|
|
103
|
+
<div className="p-6 rounded-xl border border-border bg-background">
|
|
104
|
+
<h3 className="text-lg font-semibold text-foreground mb-2">
|
|
105
|
+
Upcoming Invoice
|
|
106
|
+
</h3>
|
|
107
|
+
<div className="flex items-center justify-between">
|
|
108
|
+
<div>
|
|
109
|
+
<p className="text-3xl font-bold text-foreground">
|
|
110
|
+
{formatPrice(billing.upcomingInvoice.amount, billing.upcomingInvoice.currency)}
|
|
111
|
+
</p>
|
|
112
|
+
<p className="text-sm text-muted-foreground">
|
|
113
|
+
Due {new Date(billing.upcomingInvoice.date).toLocaleDateString()}
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
case "payment-methods":
|
|
123
|
+
return (
|
|
124
|
+
<PaymentMethodsList
|
|
125
|
+
paymentMethods={billing.paymentMethods}
|
|
126
|
+
onAddNew={() => console.log("Add payment method")}
|
|
127
|
+
onSetDefault={(id) => console.log("Set default:", id)}
|
|
128
|
+
onRemove={(id) => console.log("Remove:", id)}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
case "invoices":
|
|
133
|
+
return (
|
|
134
|
+
<div className="space-y-4">
|
|
135
|
+
{billing.recentInvoices.length > 0 ? (
|
|
136
|
+
billing.recentInvoices.map((invoice) => (
|
|
137
|
+
<InvoiceCard
|
|
138
|
+
key={invoice.id}
|
|
139
|
+
invoice={invoice}
|
|
140
|
+
onClick={(inv) => console.log("View invoice:", inv)}
|
|
141
|
+
/>
|
|
142
|
+
))
|
|
143
|
+
) : (
|
|
144
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
145
|
+
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
146
|
+
<p>No invoices yet</p>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
case "plan":
|
|
153
|
+
return (
|
|
154
|
+
<div className="text-center py-12">
|
|
155
|
+
<p className="text-muted-foreground">Plan management coming soon...</p>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div className="w-full">
|
|
166
|
+
{/* Tabs */}
|
|
167
|
+
{showTabs && (
|
|
168
|
+
<div className="border-b border-border mb-6">
|
|
169
|
+
<div className="flex gap-6">
|
|
170
|
+
{tabs.map((tab) => {
|
|
171
|
+
const Icon = tab.icon;
|
|
172
|
+
return (
|
|
173
|
+
<button
|
|
174
|
+
key={tab.id}
|
|
175
|
+
onClick={() => onTabChange?.(tab.id)}
|
|
176
|
+
className={cn(
|
|
177
|
+
"flex items-center gap-2 pb-4 border-b-2 transition-colors",
|
|
178
|
+
activeTab === tab.id
|
|
179
|
+
? "border-primary text-foreground"
|
|
180
|
+
: "border-transparent text-muted-foreground hover:text-foreground"
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<Icon className="h-4 w-4" />
|
|
184
|
+
<span className="font-medium">{tab.label}</span>
|
|
185
|
+
</button>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Content */}
|
|
193
|
+
{renderContent()}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export default BillingPortal;
|