@umituz/web-dashboard 2.2.0 → 2.4.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 +12 -1
- package/src/domains/analytics/components/AnalyticsLayout.tsx +100 -64
- package/src/domains/analytics/types/analytics.ts +45 -0
- package/src/domains/auth/components/AuthLayout.tsx +1 -0
- package/src/domains/auth/components/LoginForm.tsx +59 -0
- package/src/domains/auth/components/RegisterForm.tsx +59 -0
- package/src/domains/auth/types/auth.ts +12 -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,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for billing and subscription system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ComponentType, ReactElement } from "react";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Billing cycle
|
|
11
|
+
*/
|
|
12
|
+
export type BillingCycle = "monthly" | "yearly";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Subscription status
|
|
16
|
+
*/
|
|
17
|
+
export type SubscriptionStatus =
|
|
18
|
+
| "active"
|
|
19
|
+
| "trialing"
|
|
20
|
+
| "past_due"
|
|
21
|
+
| "canceled"
|
|
22
|
+
| "unpaid"
|
|
23
|
+
| "incomplete";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Plan type
|
|
27
|
+
*/
|
|
28
|
+
export type PlanType = "free" | "basic" | "pro" | "enterprise" | "custom";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Currency
|
|
32
|
+
*/
|
|
33
|
+
export type Currency = "USD" | "EUR" | "GBP" | "TRY" | "JPY";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Plan pricing tier
|
|
37
|
+
*/
|
|
38
|
+
export interface PlanTier {
|
|
39
|
+
/** Plan ID */
|
|
40
|
+
id: string;
|
|
41
|
+
/** Plan type */
|
|
42
|
+
type: PlanType;
|
|
43
|
+
/** Plan name */
|
|
44
|
+
name: string;
|
|
45
|
+
/** Plan description */
|
|
46
|
+
description: string;
|
|
47
|
+
/** Badge text */
|
|
48
|
+
badge?: string;
|
|
49
|
+
/** Badge color */
|
|
50
|
+
badgeColor?: string;
|
|
51
|
+
/** Monthly price */
|
|
52
|
+
monthlyPrice: number;
|
|
53
|
+
/** Yearly price */
|
|
54
|
+
yearlyPrice: number;
|
|
55
|
+
/** Currency */
|
|
56
|
+
currency: Currency;
|
|
57
|
+
/** Features list */
|
|
58
|
+
features: (string | { text: string; bold?: boolean; included?: boolean })[];
|
|
59
|
+
/** Highlight/recommend */
|
|
60
|
+
highlight?: boolean;
|
|
61
|
+
/** Maximum users/seats */
|
|
62
|
+
maxUsers?: number;
|
|
63
|
+
/** Storage limit in GB */
|
|
64
|
+
storageLimit?: number;
|
|
65
|
+
/** API call limit */
|
|
66
|
+
apiLimit?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscription info
|
|
71
|
+
*/
|
|
72
|
+
export interface Subscription {
|
|
73
|
+
/** Subscription ID */
|
|
74
|
+
id: string;
|
|
75
|
+
/** Plan ID */
|
|
76
|
+
planId: string;
|
|
77
|
+
/** Plan details */
|
|
78
|
+
plan: PlanTier;
|
|
79
|
+
/** Subscription status */
|
|
80
|
+
status: SubscriptionStatus;
|
|
81
|
+
/** Billing cycle */
|
|
82
|
+
cycle: BillingCycle;
|
|
83
|
+
/** Current period start */
|
|
84
|
+
currentPeriodStart: string;
|
|
85
|
+
/** Current period end */
|
|
86
|
+
currentPeriodEnd: string;
|
|
87
|
+
/** Cancel at period end */
|
|
88
|
+
cancelAtPeriodEnd?: boolean;
|
|
89
|
+
/** Trial end date */
|
|
90
|
+
trialEnd?: string;
|
|
91
|
+
/** User/seat count */
|
|
92
|
+
seats?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Payment method type
|
|
97
|
+
*/
|
|
98
|
+
export type PaymentMethodType = "card" | "bank_account" | "wallet";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Payment method
|
|
102
|
+
*/
|
|
103
|
+
export interface PaymentMethod {
|
|
104
|
+
/** Payment method ID */
|
|
105
|
+
id: string;
|
|
106
|
+
/** Type */
|
|
107
|
+
type: PaymentMethodType;
|
|
108
|
+
/** Is default */
|
|
109
|
+
isDefault: boolean;
|
|
110
|
+
/** Card details */
|
|
111
|
+
card?: {
|
|
112
|
+
/** Last 4 digits */
|
|
113
|
+
last4: string;
|
|
114
|
+
/** Brand (Visa, Mastercard) */
|
|
115
|
+
brand: string;
|
|
116
|
+
/** Expiry month */
|
|
117
|
+
expiryMonth: number;
|
|
118
|
+
/** Expiry year */
|
|
119
|
+
expiryYear: number;
|
|
120
|
+
/** Cardholder name */
|
|
121
|
+
name?: string;
|
|
122
|
+
};
|
|
123
|
+
/** Bank account details */
|
|
124
|
+
bankAccount?: {
|
|
125
|
+
/** Last 4 digits */
|
|
126
|
+
last4: string;
|
|
127
|
+
/** Bank name */
|
|
128
|
+
bankName: string;
|
|
129
|
+
/** Account type */
|
|
130
|
+
accountType?: "checking" | "savings";
|
|
131
|
+
};
|
|
132
|
+
/** Created date */
|
|
133
|
+
createdAt: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Invoice status
|
|
138
|
+
*/
|
|
139
|
+
export type InvoiceStatus = "draft" | "open" | "paid" | "void" | "uncollectible";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Invoice item
|
|
143
|
+
*/
|
|
144
|
+
export interface InvoiceItem {
|
|
145
|
+
/** Item description */
|
|
146
|
+
description: string;
|
|
147
|
+
/** Quantity */
|
|
148
|
+
quantity: number;
|
|
149
|
+
/** Unit price */
|
|
150
|
+
unitPrice: number;
|
|
151
|
+
/** Amount */
|
|
152
|
+
amount: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Invoice
|
|
157
|
+
*/
|
|
158
|
+
export interface Invoice {
|
|
159
|
+
/** Invoice ID */
|
|
160
|
+
id: string;
|
|
161
|
+
/** Invoice number */
|
|
162
|
+
number: string;
|
|
163
|
+
/** Amount */
|
|
164
|
+
amount: number;
|
|
165
|
+
/** Currency */
|
|
166
|
+
currency: Currency;
|
|
167
|
+
/** Status */
|
|
168
|
+
status: InvoiceStatus;
|
|
169
|
+
/** Invoice date */
|
|
170
|
+
date: string;
|
|
171
|
+
/** Due date */
|
|
172
|
+
dueDate: string;
|
|
173
|
+
/** Paid date */
|
|
174
|
+
paidAt?: string;
|
|
175
|
+
/** Invoice URL */
|
|
176
|
+
invoiceUrl?: string;
|
|
177
|
+
/** PDF download URL */
|
|
178
|
+
pdfUrl?: string;
|
|
179
|
+
/** Line items */
|
|
180
|
+
items?: InvoiceItem[];
|
|
181
|
+
/** Subtotal */
|
|
182
|
+
subtotal?: number;
|
|
183
|
+
/** Tax amount */
|
|
184
|
+
tax?: number;
|
|
185
|
+
/** Total amount */
|
|
186
|
+
total?: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Usage metric
|
|
191
|
+
*/
|
|
192
|
+
export interface UsageMetric {
|
|
193
|
+
/** Metric ID */
|
|
194
|
+
id: string;
|
|
195
|
+
/** Metric name */
|
|
196
|
+
name: string;
|
|
197
|
+
/** Current usage */
|
|
198
|
+
current: number;
|
|
199
|
+
/** Limit */
|
|
200
|
+
limit: number;
|
|
201
|
+
/** Unit */
|
|
202
|
+
unit: string;
|
|
203
|
+
/** Reset period */
|
|
204
|
+
resetPeriod: "daily" | "monthly" | "yearly";
|
|
205
|
+
/** Reset date */
|
|
206
|
+
resetAt?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Billing summary
|
|
211
|
+
*/
|
|
212
|
+
export interface BillingSummary {
|
|
213
|
+
/** Current subscription */
|
|
214
|
+
subscription: Subscription;
|
|
215
|
+
/** Payment methods */
|
|
216
|
+
paymentMethods: PaymentMethod[];
|
|
217
|
+
/** Default payment method */
|
|
218
|
+
defaultPaymentMethod?: PaymentMethod;
|
|
219
|
+
/** Upcoming invoice */
|
|
220
|
+
upcomingInvoice?: {
|
|
221
|
+
amount: number;
|
|
222
|
+
currency: Currency;
|
|
223
|
+
date: string;
|
|
224
|
+
};
|
|
225
|
+
/** Usage metrics */
|
|
226
|
+
usage: UsageMetric[];
|
|
227
|
+
/** Recent invoices */
|
|
228
|
+
recentInvoices: Invoice[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Plan comparison props
|
|
233
|
+
*/
|
|
234
|
+
export interface PlanComparisonProps {
|
|
235
|
+
/** Available plans */
|
|
236
|
+
plans: PlanTier[];
|
|
237
|
+
/** Selected plan ID */
|
|
238
|
+
selectedPlan?: string;
|
|
239
|
+
/** Billing cycle */
|
|
240
|
+
cycle: BillingCycle;
|
|
241
|
+
/** Show yearly toggle */
|
|
242
|
+
showCycleToggle?: boolean;
|
|
243
|
+
/** Show features */
|
|
244
|
+
showFeatures?: boolean;
|
|
245
|
+
/** On plan select */
|
|
246
|
+
onPlanSelect?: (planId: string) => void;
|
|
247
|
+
/** On cycle change */
|
|
248
|
+
onCycleChange?: (cycle: BillingCycle) => void;
|
|
249
|
+
/** Loading state */
|
|
250
|
+
loading?: boolean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Payment methods list props
|
|
255
|
+
*/
|
|
256
|
+
export interface PaymentMethodsListProps {
|
|
257
|
+
/** Payment methods */
|
|
258
|
+
paymentMethods: PaymentMethod[];
|
|
259
|
+
/** Loading state */
|
|
260
|
+
loading?: boolean;
|
|
261
|
+
/** On set default */
|
|
262
|
+
onSetDefault?: (methodId: string) => void;
|
|
263
|
+
/** On remove */
|
|
264
|
+
onRemove?: (methodId: string) => void;
|
|
265
|
+
/** On add new */
|
|
266
|
+
onAddNew?: () => void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Invoice card props
|
|
271
|
+
*/
|
|
272
|
+
export interface InvoiceCardProps {
|
|
273
|
+
/** Invoice data */
|
|
274
|
+
invoice: Invoice;
|
|
275
|
+
/** Compact view */
|
|
276
|
+
compact?: boolean;
|
|
277
|
+
/** On click */
|
|
278
|
+
onClick?: (invoice: Invoice) => void;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Usage card props
|
|
283
|
+
*/
|
|
284
|
+
export interface UsageCardProps {
|
|
285
|
+
/** Usage metric */
|
|
286
|
+
metric: UsageMetric;
|
|
287
|
+
/** Show progress bar */
|
|
288
|
+
showProgress?: boolean;
|
|
289
|
+
/** Show limit */
|
|
290
|
+
showLimit?: boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Billing portal props
|
|
295
|
+
*/
|
|
296
|
+
export interface BillingPortalProps {
|
|
297
|
+
/** Billing summary */
|
|
298
|
+
billing: BillingSummary;
|
|
299
|
+
/** Loading state */
|
|
300
|
+
loading?: boolean;
|
|
301
|
+
/** Error message */
|
|
302
|
+
error?: string;
|
|
303
|
+
/** Show tabs */
|
|
304
|
+
showTabs?: boolean;
|
|
305
|
+
/** Active tab */
|
|
306
|
+
activeTab?: string;
|
|
307
|
+
/** On tab change */
|
|
308
|
+
onTabChange?: (tab: string) => void;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Billing layout props
|
|
313
|
+
*/
|
|
314
|
+
export interface BillingLayoutProps {
|
|
315
|
+
/** Billing configuration */
|
|
316
|
+
config: BillingConfig;
|
|
317
|
+
/** Children content */
|
|
318
|
+
children?: React.ReactNode;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Billing configuration
|
|
323
|
+
*/
|
|
324
|
+
export interface BillingConfig {
|
|
325
|
+
/** Brand name */
|
|
326
|
+
brandName: string;
|
|
327
|
+
/** Available plans */
|
|
328
|
+
plans: PlanTier[];
|
|
329
|
+
/** Supported currencies */
|
|
330
|
+
currencies?: Currency[];
|
|
331
|
+
/** Default currency */
|
|
332
|
+
defaultCurrency?: Currency;
|
|
333
|
+
/** Enable yearly discounts */
|
|
334
|
+
enableYearlyDiscount?: boolean;
|
|
335
|
+
/** Yearly discount percentage */
|
|
336
|
+
yearlyDiscount?: number;
|
|
337
|
+
/** Tax rate */
|
|
338
|
+
taxRate?: number;
|
|
339
|
+
/** Support email */
|
|
340
|
+
supportEmail?: string;
|
|
341
|
+
/** Cancel subscription route */
|
|
342
|
+
cancelRoute?: string;
|
|
343
|
+
/** Update payment route */
|
|
344
|
+
updatePaymentRoute?: string;
|
|
345
|
+
/** Invoice history route */
|
|
346
|
+
invoiceHistoryRoute?: string;
|
|
347
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Types
|
|
3
|
+
*
|
|
4
|
+
* Export all billing-related types
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type {
|
|
8
|
+
BillingCycle,
|
|
9
|
+
SubscriptionStatus,
|
|
10
|
+
PlanType,
|
|
11
|
+
Currency,
|
|
12
|
+
PlanTier,
|
|
13
|
+
Subscription,
|
|
14
|
+
PaymentMethodType,
|
|
15
|
+
PaymentMethod,
|
|
16
|
+
InvoiceStatus,
|
|
17
|
+
InvoiceItem,
|
|
18
|
+
Invoice,
|
|
19
|
+
UsageMetric,
|
|
20
|
+
BillingSummary,
|
|
21
|
+
PlanComparisonProps,
|
|
22
|
+
PaymentMethodsListProps,
|
|
23
|
+
InvoiceCardProps,
|
|
24
|
+
UsageCardProps,
|
|
25
|
+
BillingPortalProps,
|
|
26
|
+
BillingLayoutProps,
|
|
27
|
+
BillingConfig,
|
|
28
|
+
} from "./billing";
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for billing operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
BillingCycle,
|
|
9
|
+
Currency,
|
|
10
|
+
PlanTier,
|
|
11
|
+
Subscription,
|
|
12
|
+
PaymentMethod,
|
|
13
|
+
Invoice,
|
|
14
|
+
InvoiceStatus,
|
|
15
|
+
UsageMetric,
|
|
16
|
+
} from "../types/billing";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Format price with currency
|
|
20
|
+
*
|
|
21
|
+
* @param amount - Amount to format
|
|
22
|
+
* @param currency - Currency code (default: USD)
|
|
23
|
+
* @returns Formatted price string
|
|
24
|
+
*/
|
|
25
|
+
export function formatPrice(amount: number, currency: Currency = "USD"): string {
|
|
26
|
+
const localeMap: Record<Currency, string> = {
|
|
27
|
+
USD: "en-US",
|
|
28
|
+
EUR: "de-DE",
|
|
29
|
+
GBP: "en-GB",
|
|
30
|
+
TRY: "tr-TR",
|
|
31
|
+
JPY: "ja-JP",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return new Intl.NumberFormat(localeMap[currency], {
|
|
35
|
+
style: "currency",
|
|
36
|
+
currency,
|
|
37
|
+
}).format(amount);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Calculate yearly discount
|
|
42
|
+
*
|
|
43
|
+
* @param monthlyPrice - Monthly price
|
|
44
|
+
* @param yearlyPrice - Yearly price
|
|
45
|
+
* @returns Discount percentage
|
|
46
|
+
*/
|
|
47
|
+
export function calculateDiscount(monthlyPrice: number, yearlyPrice: number): number {
|
|
48
|
+
const yearlyMonthly = yearlyPrice / 12;
|
|
49
|
+
const discount = ((monthlyPrice - yearlyMonthly) / monthlyPrice) * 100;
|
|
50
|
+
return Math.round(discount);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate prorated amount
|
|
55
|
+
*
|
|
56
|
+
* @param amount - Full amount
|
|
57
|
+
* @param daysUsed - Days used in period
|
|
58
|
+
* @param totalDays - Total days in period
|
|
59
|
+
* @returns Prorated amount
|
|
60
|
+
*/
|
|
61
|
+
export function calculateProratedAmount(
|
|
62
|
+
amount: number,
|
|
63
|
+
daysUsed: number,
|
|
64
|
+
totalDays: number
|
|
65
|
+
): number {
|
|
66
|
+
if (totalDays === 0) return 0;
|
|
67
|
+
return (amount / totalDays) * daysUsed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get plan price by cycle
|
|
72
|
+
*
|
|
73
|
+
* @param plan - Plan tier
|
|
74
|
+
* @param cycle - Billing cycle
|
|
75
|
+
* @returns Price for cycle
|
|
76
|
+
*/
|
|
77
|
+
export function getPlanPrice(plan: PlanTier, cycle: BillingCycle): number {
|
|
78
|
+
return cycle === "monthly" ? plan.monthlyPrice : plan.yearlyPrice;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Calculate usage percentage
|
|
83
|
+
*
|
|
84
|
+
* @param metric - Usage metric
|
|
85
|
+
* @returns Usage percentage (0-100)
|
|
86
|
+
*/
|
|
87
|
+
export function calculateUsagePercentage(metric: UsageMetric): number {
|
|
88
|
+
if (metric.limit === 0) return 0;
|
|
89
|
+
return Math.min((metric.current / metric.limit) * 100, 100);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if usage is near limit
|
|
94
|
+
*
|
|
95
|
+
* @param metric - Usage metric
|
|
96
|
+
* @param threshold - Warning threshold (default: 80)
|
|
97
|
+
* @returns Whether near limit
|
|
98
|
+
*/
|
|
99
|
+
export function isNearLimit(metric: UsageMetric, threshold: number = 80): boolean {
|
|
100
|
+
return calculateUsagePercentage(metric) >= threshold;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get subscription status color
|
|
105
|
+
*
|
|
106
|
+
* @param status - Subscription status
|
|
107
|
+
* @returns Color class
|
|
108
|
+
*/
|
|
109
|
+
export function getStatusColor(status: SubscriptionStatus): string {
|
|
110
|
+
const colorMap: Record<SubscriptionStatus, string> = {
|
|
111
|
+
active: "text-green-600 dark:text-green-500",
|
|
112
|
+
trialing: "text-blue-600 dark:text-blue-500",
|
|
113
|
+
past_due: "text-orange-600 dark:text-orange-500",
|
|
114
|
+
canceled: "text-gray-600 dark:text-gray-500",
|
|
115
|
+
unpaid: "text-red-600 dark:text-red-500",
|
|
116
|
+
incomplete: "text-yellow-600 dark:text-yellow-500",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return colorMap[status] || "text-gray-600";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get subscription status label
|
|
124
|
+
*
|
|
125
|
+
* @param status - Subscription status
|
|
126
|
+
* @returns Human readable label
|
|
127
|
+
*/
|
|
128
|
+
export function getStatusLabel(status: SubscriptionStatus): string {
|
|
129
|
+
const labelMap: Record<SubscriptionStatus, string> = {
|
|
130
|
+
active: "Active",
|
|
131
|
+
trialing: "Trial",
|
|
132
|
+
past_due: "Past Due",
|
|
133
|
+
canceled: "Canceled",
|
|
134
|
+
unpaid: "Unpaid",
|
|
135
|
+
incomplete: "Incomplete",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return labelMap[status] || status;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get invoice status color
|
|
143
|
+
*
|
|
144
|
+
* @param status - Invoice status
|
|
145
|
+
* @returns Color class
|
|
146
|
+
*/
|
|
147
|
+
export function getInvoiceStatusColor(status: InvoiceStatus): string {
|
|
148
|
+
const colorMap: Record<InvoiceStatus, string> = {
|
|
149
|
+
draft: "text-gray-600 dark:text-gray-500",
|
|
150
|
+
open: "text-orange-600 dark:text-orange-500",
|
|
151
|
+
paid: "text-green-600 dark:text-green-500",
|
|
152
|
+
void: "text-gray-600 dark:text-gray-500",
|
|
153
|
+
uncollectible: "text-red-600 dark:text-red-500",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return colorMap[status] || "text-gray-600";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get invoice status label
|
|
161
|
+
*
|
|
162
|
+
* @param status - Invoice status
|
|
163
|
+
* @returns Human readable label
|
|
164
|
+
*/
|
|
165
|
+
export function getInvoiceStatusLabel(status: InvoiceStatus): string {
|
|
166
|
+
const labelMap: Record<InvoiceStatus, string> = {
|
|
167
|
+
draft: "Draft",
|
|
168
|
+
open: "Open",
|
|
169
|
+
paid: "Paid",
|
|
170
|
+
void: "Void",
|
|
171
|
+
uncollectible: "Uncollectible",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return labelMap[status] || status;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Format card number
|
|
179
|
+
*
|
|
180
|
+
* @param last4 - Last 4 digits
|
|
181
|
+
* @param brand - Card brand
|
|
182
|
+
* @returns Formatted card display
|
|
183
|
+
*/
|
|
184
|
+
export function formatCardNumber(last4: string, brand: string): string {
|
|
185
|
+
return `${brand.toUpperCase()} •••• ${last4}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Format expiry date
|
|
190
|
+
*
|
|
191
|
+
* @param month - Expiry month
|
|
192
|
+
* @param year - Expiry year
|
|
193
|
+
* @returns Formatted expiry (MM/YY)
|
|
194
|
+
*/
|
|
195
|
+
export function formatExpiry(month: number, year: number): string {
|
|
196
|
+
return `${String(month).padStart(2, "0")}/${String(year).slice(-2)}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Calculate remaining days
|
|
201
|
+
*
|
|
202
|
+
* @param endDate - End date string
|
|
203
|
+
* @returns Days remaining
|
|
204
|
+
*/
|
|
205
|
+
export function getDaysRemaining(endDate: string): number {
|
|
206
|
+
const end = new Date(endDate);
|
|
207
|
+
const now = new Date();
|
|
208
|
+
const diff = end.getTime() - now.getTime();
|
|
209
|
+
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if trial is expiring soon
|
|
214
|
+
*
|
|
215
|
+
* @param trialEnd - Trial end date
|
|
216
|
+
* @param daysThreshold - Days threshold (default: 7)
|
|
217
|
+
* @returns Whether trial is expiring soon
|
|
218
|
+
*/
|
|
219
|
+
export function isTrialExpiringSoon(trialEnd: string, daysThreshold: number = 7): boolean {
|
|
220
|
+
const daysRemaining = getDaysRemaining(trialEnd);
|
|
221
|
+
return daysRemaining > 0 && daysRemaining <= daysThreshold;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate next billing date
|
|
226
|
+
*
|
|
227
|
+
* @param currentPeriodEnd - Current period end
|
|
228
|
+
* @param cycle - Billing cycle
|
|
229
|
+
* @returns Next billing date
|
|
230
|
+
*/
|
|
231
|
+
export function getNextBillingDate(
|
|
232
|
+
currentPeriodEnd: string,
|
|
233
|
+
cycle: BillingCycle
|
|
234
|
+
): Date {
|
|
235
|
+
const nextDate = new Date(currentPeriodEnd);
|
|
236
|
+
|
|
237
|
+
if (cycle === "monthly") {
|
|
238
|
+
nextDate.setMonth(nextDate.getMonth() + 1);
|
|
239
|
+
} else {
|
|
240
|
+
nextDate.setFullYear(nextDate.getFullYear() + 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return nextDate;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Group invoices by status
|
|
248
|
+
*
|
|
249
|
+
* @param invoices - Array of invoices
|
|
250
|
+
* @returns Grouped invoices map
|
|
251
|
+
*/
|
|
252
|
+
export function groupInvoicesByStatus(
|
|
253
|
+
invoices: Invoice[]
|
|
254
|
+
): Record<InvoiceStatus, Invoice[]> {
|
|
255
|
+
const grouped: Record<string, Invoice[]> = {
|
|
256
|
+
draft: [],
|
|
257
|
+
open: [],
|
|
258
|
+
paid: [],
|
|
259
|
+
void: [],
|
|
260
|
+
uncollectible: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
invoices.forEach((invoice) => {
|
|
264
|
+
if (grouped[invoice.status]) {
|
|
265
|
+
grouped[invoice.status].push(invoice);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return grouped as Record<InvoiceStatus, Invoice[]>;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Calculate total invoice amount
|
|
274
|
+
*
|
|
275
|
+
* @param invoices - Array of invoices
|
|
276
|
+
* @param status - Optional status filter
|
|
277
|
+
* @returns Total amount
|
|
278
|
+
*/
|
|
279
|
+
export function calculateInvoiceTotal(
|
|
280
|
+
invoices: Invoice[],
|
|
281
|
+
status?: InvoiceStatus
|
|
282
|
+
): number {
|
|
283
|
+
const filtered = status ? invoices.filter((inv) => inv.status === status) : invoices;
|
|
284
|
+
return filtered.reduce((sum, inv) => sum + inv.amount, 0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Sort invoices by date
|
|
289
|
+
*
|
|
290
|
+
* @param invoices - Array of invoices
|
|
291
|
+
* @param order - Sort order (default: desc)
|
|
292
|
+
* @returns Sorted invoices
|
|
293
|
+
*/
|
|
294
|
+
export function sortInvoicesByDate(
|
|
295
|
+
invoices: Invoice[],
|
|
296
|
+
order: "asc" | "desc" = "desc"
|
|
297
|
+
): Invoice[] {
|
|
298
|
+
return [...invoices].sort((a, b) => {
|
|
299
|
+
const dateA = new Date(a.date).getTime();
|
|
300
|
+
const dateB = new Date(b.date).getTime();
|
|
301
|
+
return order === "asc" ? dateA - dateB : dateB - dateB;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Format feature list item
|
|
307
|
+
*
|
|
308
|
+
* @param feature - Feature item (string or object)
|
|
309
|
+
* @returns Formatted feature
|
|
310
|
+
*/
|
|
311
|
+
export function formatFeature(
|
|
312
|
+
feature: string | { text: string; bold?: boolean; included?: boolean }
|
|
313
|
+
): { text: string; bold?: boolean; included?: boolean } {
|
|
314
|
+
if (typeof feature === "string") {
|
|
315
|
+
return { text: feature, included: true };
|
|
316
|
+
}
|
|
317
|
+
return feature;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Check if plan is popular
|
|
322
|
+
*
|
|
323
|
+
* @param plan - Plan tier
|
|
324
|
+
* @param plans - All available plans
|
|
325
|
+
* @returns Whether plan is popular (middle tier)
|
|
326
|
+
*/
|
|
327
|
+
export function isPopularPlan(plan: PlanTier, plans: PlanTier[]): boolean {
|
|
328
|
+
const middleIndex = Math.floor(plans.length / 2);
|
|
329
|
+
return plans[middleIndex]?.id === plan.id;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Generate trial days text
|
|
334
|
+
*
|
|
335
|
+
* @param trialEnd - Trial end date
|
|
336
|
+
* @returns Days remaining text
|
|
337
|
+
*/
|
|
338
|
+
export function getTrialDaysText(trialEnd: string): string {
|
|
339
|
+
const days = getDaysRemaining(trialEnd);
|
|
340
|
+
|
|
341
|
+
if (days === 0) return "Trial ends today";
|
|
342
|
+
if (days === 1) return "1 day left";
|
|
343
|
+
return `${days} days left`;
|
|
344
|
+
}
|