@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.
Files changed (27) hide show
  1. package/package.json +24 -1
  2. package/src/domains/analytics/components/AnalyticsCard.tsx +80 -0
  3. package/src/domains/analytics/components/AnalyticsChart.tsx +184 -0
  4. package/src/domains/analytics/components/AnalyticsLayout.tsx +150 -0
  5. package/src/domains/analytics/components/MetricCard.tsx +108 -0
  6. package/src/domains/analytics/components/index.ts +10 -0
  7. package/src/domains/analytics/hooks/index.ts +7 -0
  8. package/src/domains/analytics/hooks/useAnalytics.ts +178 -0
  9. package/src/domains/analytics/index.ts +62 -0
  10. package/src/domains/analytics/types/analytics.ts +291 -0
  11. package/src/domains/analytics/types/index.ts +26 -0
  12. package/src/domains/analytics/utils/analytics.ts +333 -0
  13. package/src/domains/analytics/utils/index.ts +26 -0
  14. package/src/domains/billing/components/BillingLayout.tsx +63 -0
  15. package/src/domains/billing/components/BillingPortal.tsx +198 -0
  16. package/src/domains/billing/components/InvoiceCard.tsx +143 -0
  17. package/src/domains/billing/components/PaymentMethodsList.tsx +116 -0
  18. package/src/domains/billing/components/PlanComparison.tsx +199 -0
  19. package/src/domains/billing/components/UsageCard.tsx +103 -0
  20. package/src/domains/billing/components/index.ts +12 -0
  21. package/src/domains/billing/hooks/index.ts +7 -0
  22. package/src/domains/billing/hooks/useBilling.ts +385 -0
  23. package/src/domains/billing/index.ts +69 -0
  24. package/src/domains/billing/types/billing.ts +347 -0
  25. package/src/domains/billing/types/index.ts +28 -0
  26. package/src/domains/billing/utils/billing.ts +344 -0
  27. 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;