payment-kit 1.20.10 → 1.20.12
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/README.md +25 -24
- package/api/src/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +253 -2
- package/api/src/libs/currency.ts +31 -0
- package/api/src/libs/discount/coupon.ts +1061 -0
- package/api/src/libs/discount/discount.ts +349 -0
- package/api/src/libs/discount/nft.ts +239 -0
- package/api/src/libs/discount/redemption.ts +636 -0
- package/api/src/libs/discount/vc.ts +73 -0
- package/api/src/libs/invoice.ts +50 -16
- package/api/src/libs/math-utils.ts +6 -0
- package/api/src/libs/price.ts +43 -0
- package/api/src/libs/session.ts +242 -57
- package/api/src/libs/subscription.ts +2 -6
- package/api/src/locales/en.ts +38 -38
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/discount-status.ts +200 -0
- package/api/src/queues/subscription.ts +98 -5
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +5 -3
- package/api/src/routes/checkout-sessions.ts +755 -64
- package/api/src/routes/connect/change-payment.ts +6 -1
- package/api/src/routes/connect/change-plan.ts +6 -1
- package/api/src/routes/connect/setup.ts +6 -1
- package/api/src/routes/connect/shared.ts +80 -9
- package/api/src/routes/connect/subscribe.ts +12 -2
- package/api/src/routes/coupons.ts +518 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +44 -3
- package/api/src/routes/meter-events.ts +2 -1
- package/api/src/routes/payment-currencies.ts +1 -0
- package/api/src/routes/promotion-codes.ts +482 -0
- package/api/src/routes/subscriptions.ts +23 -2
- package/api/src/store/migrations/20250904-discount.ts +136 -0
- package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
- package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
- package/api/src/store/models/checkout-session.ts +12 -0
- package/api/src/store/models/coupon.ts +144 -4
- package/api/src/store/models/discount.ts +23 -10
- package/api/src/store/models/index.ts +13 -2
- package/api/src/store/models/promotion-code.ts +295 -18
- package/api/src/store/models/types.ts +30 -1
- package/api/tests/libs/session.spec.ts +48 -27
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +38 -38
- package/package.json +20 -20
- package/src/app.tsx +2 -0
- package/src/components/customer/link.tsx +1 -1
- package/src/components/discount/discount-info.tsx +178 -0
- package/src/components/invoice/table.tsx +140 -48
- package/src/components/invoice-pdf/styles.ts +6 -0
- package/src/components/invoice-pdf/template.tsx +59 -33
- package/src/components/metadata/form.tsx +14 -5
- package/src/components/payment-link/actions.tsx +42 -0
- package/src/components/price/form.tsx +91 -65
- package/src/components/product/vendor-config.tsx +5 -3
- package/src/components/promotion/active-redemptions.tsx +534 -0
- package/src/components/promotion/currency-multi-select.tsx +350 -0
- package/src/components/promotion/currency-restrictions.tsx +117 -0
- package/src/components/promotion/product-select.tsx +292 -0
- package/src/components/promotion/promotion-code-form.tsx +534 -0
- package/src/components/subscription/portal/list.tsx +6 -1
- package/src/components/subscription/vendor-service-list.tsx +13 -2
- package/src/locales/en.tsx +253 -26
- package/src/locales/zh.tsx +222 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
- package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
- package/src/pages/admin/products/coupons/create.tsx +612 -0
- package/src/pages/admin/products/coupons/detail.tsx +538 -0
- package/src/pages/admin/products/coupons/edit.tsx +127 -0
- package/src/pages/admin/products/coupons/index.tsx +210 -3
- package/src/pages/admin/products/index.tsx +22 -3
- package/src/pages/admin/products/products/detail.tsx +12 -2
- package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
- package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
- package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
- package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
- package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
- package/src/pages/admin/products/vendors/index.tsx +17 -5
- package/src/pages/customer/subscription/detail.tsx +5 -0
- package/vite.config.ts +4 -3
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/* eslint-disable react/display-name, react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { api, formatTime, Link, Table } from '@blocklet/payment-react';
|
|
4
|
+
import { Box, Tab, Tabs, Typography, Chip } from '@mui/material';
|
|
5
|
+
import { useRequest } from 'ahooks';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { useNavigate } from 'react-router-dom';
|
|
8
|
+
import CustomerLink from '../customer/link';
|
|
9
|
+
import SubscriptionDescription from '../subscription/description';
|
|
10
|
+
import SubscriptionStatus from '../subscription/status';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
couponId?: string;
|
|
14
|
+
promotionCodeId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Type guard to ensure at least one ID is provided
|
|
18
|
+
function validateProps(props: Props): asserts props is Props & ({ couponId: string } | { promotionCodeId: string }) {
|
|
19
|
+
if ((!props.couponId || props.couponId === '') && (!props.promotionCodeId || props.promotionCodeId === '')) {
|
|
20
|
+
throw new Error('Either couponId or promotionCodeId must be provided');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PaginationParams {
|
|
25
|
+
page: number;
|
|
26
|
+
pageSize: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fetchActiveRedemptions = (
|
|
30
|
+
id: string,
|
|
31
|
+
params: PaginationParams,
|
|
32
|
+
type?: 'customer' | 'subscription',
|
|
33
|
+
entityType: 'coupon' | 'promotion-code' = 'coupon'
|
|
34
|
+
): Promise<any> => {
|
|
35
|
+
const query = new URLSearchParams({
|
|
36
|
+
page: params.page.toString(),
|
|
37
|
+
pageSize: params.pageSize.toString(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (type) {
|
|
41
|
+
query.append('type', type);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const endpoint =
|
|
45
|
+
entityType === 'coupon' ? `/api/coupons/${id}/redemptions` : `/api/promotion-codes/${id}/redemptions`;
|
|
46
|
+
return api.get(`${endpoint}?${query}`).then((res) => res.data);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function ActiveRedemptions({ couponId = '', promotionCodeId = '' }: Props) {
|
|
50
|
+
// Validate props at the beginning
|
|
51
|
+
const props = { couponId, promotionCodeId };
|
|
52
|
+
validateProps(props);
|
|
53
|
+
const { t } = useLocaleContext();
|
|
54
|
+
const navigate = useNavigate();
|
|
55
|
+
const [tabValue, setTabValue] = useState(0);
|
|
56
|
+
const [customerPagination, setCustomerPagination] = useState({ page: 0, pageSize: 10 });
|
|
57
|
+
const [subscriptionPagination, setSubscriptionPagination] = useState({ page: 0, pageSize: 10 });
|
|
58
|
+
|
|
59
|
+
// Determine which ID and entity type to use
|
|
60
|
+
const id = couponId && couponId !== '' ? couponId : promotionCodeId!; // Safe to use ! after validation
|
|
61
|
+
const entityType: 'coupon' | 'promotion-code' = couponId && couponId !== '' ? 'coupon' : 'promotion-code';
|
|
62
|
+
|
|
63
|
+
// Fetch data based on current tab and pagination
|
|
64
|
+
const currentPagination = tabValue === 0 ? customerPagination : subscriptionPagination;
|
|
65
|
+
const currentType = tabValue === 0 ? 'customer' : 'subscription';
|
|
66
|
+
|
|
67
|
+
const { data = { customers: [], subscriptions: [], count: 0 }, loading } = useRequest(
|
|
68
|
+
() =>
|
|
69
|
+
fetchActiveRedemptions(
|
|
70
|
+
id,
|
|
71
|
+
{
|
|
72
|
+
page: currentPagination.page + 1, // API pages start from 1, table pages start from 0
|
|
73
|
+
pageSize: currentPagination.pageSize,
|
|
74
|
+
},
|
|
75
|
+
currentType,
|
|
76
|
+
entityType
|
|
77
|
+
),
|
|
78
|
+
{
|
|
79
|
+
refreshDeps: [id, currentPagination.page, currentPagination.pageSize, tabValue],
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const handleTabChange = (_: React.SyntheticEvent, newValue: number) => {
|
|
84
|
+
setTabValue(newValue);
|
|
85
|
+
// Reset pagination when switching tabs
|
|
86
|
+
if (newValue === 0) {
|
|
87
|
+
setCustomerPagination({ page: 0, pageSize: 10 });
|
|
88
|
+
} else {
|
|
89
|
+
setSubscriptionPagination({ page: 0, pageSize: 10 });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleCustomerTableChange = ({ page, rowsPerPage }: any) => {
|
|
94
|
+
setCustomerPagination({ page, pageSize: rowsPerPage });
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleSubscriptionTableChange = ({ page, rowsPerPage }: any) => {
|
|
98
|
+
setSubscriptionPagination({ page, pageSize: rowsPerPage });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Build customer columns dynamically based on entity type
|
|
102
|
+
const buildCustomerColumns = () => {
|
|
103
|
+
const baseColumns = [
|
|
104
|
+
{
|
|
105
|
+
label: t('common.name'),
|
|
106
|
+
name: 'name',
|
|
107
|
+
options: {
|
|
108
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
109
|
+
const customer = data.customers?.[index];
|
|
110
|
+
return <CustomerLink customer={customer} size="small" />;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: t('common.email'),
|
|
116
|
+
name: 'email',
|
|
117
|
+
options: {
|
|
118
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
119
|
+
const customer = data.customers?.[index];
|
|
120
|
+
return customer?.email || '—';
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: t('admin.coupon.usageStats'),
|
|
126
|
+
name: 'usage_stats',
|
|
127
|
+
options: {
|
|
128
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
129
|
+
const customer = data.customers?.[index];
|
|
130
|
+
const stats = customer?.coupon_usage_stats;
|
|
131
|
+
if (!stats) return '—';
|
|
132
|
+
return <Typography variant="body2">{stats.total_discount_records}</Typography>;
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
// Only show promotion codes column when in coupon mode (not in promotion code detail page)
|
|
139
|
+
if (entityType === 'coupon') {
|
|
140
|
+
baseColumns.push({
|
|
141
|
+
label: t('admin.coupon.promotionCodesUsed'),
|
|
142
|
+
name: 'promotion_codes',
|
|
143
|
+
options: {
|
|
144
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
145
|
+
const customer = data.customers?.[index];
|
|
146
|
+
const promoCodes = customer?.coupon_usage_stats?.promotion_codes_used || [];
|
|
147
|
+
const discountRecords = customer?.discount_records || [];
|
|
148
|
+
|
|
149
|
+
if (promoCodes.length === 0) return '—';
|
|
150
|
+
|
|
151
|
+
// Calculate usage count for each promotion code from actual discount records
|
|
152
|
+
const promoUsageMap = new Map();
|
|
153
|
+
discountRecords.forEach((record: any) => {
|
|
154
|
+
if (record.promotion_code) {
|
|
155
|
+
const key = record.promotion_code.id || record.promotion_code.code;
|
|
156
|
+
promoUsageMap.set(key, (promoUsageMap.get(key) || 0) + 1);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, maxWidth: '200px' }}>
|
|
162
|
+
{promoCodes.map((promo: any) => {
|
|
163
|
+
// Use actual discount record count instead of promo.times_redeemed
|
|
164
|
+
const actualUsageCount = promoUsageMap.get(promo.id || promo.code) || 0;
|
|
165
|
+
return (
|
|
166
|
+
<Chip
|
|
167
|
+
key={`${promo.id || promo.code}`}
|
|
168
|
+
label={`${promo.code} (${actualUsageCount})`}
|
|
169
|
+
size="small"
|
|
170
|
+
variant="outlined"
|
|
171
|
+
onClick={() => navigate(`/admin/products/promotion-codes/${promo.id}`)}
|
|
172
|
+
sx={{
|
|
173
|
+
cursor: 'pointer',
|
|
174
|
+
'&:hover': {
|
|
175
|
+
backgroundColor: 'action.hover',
|
|
176
|
+
},
|
|
177
|
+
}}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
})}
|
|
181
|
+
</Box>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add total savings and usage period columns
|
|
189
|
+
baseColumns.push(
|
|
190
|
+
{
|
|
191
|
+
label: t('admin.coupon.totalSavings'),
|
|
192
|
+
name: 'total_savings',
|
|
193
|
+
options: {
|
|
194
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
195
|
+
const customer = data.customers?.[index];
|
|
196
|
+
const totalSavingsByCurrency = (customer?.coupon_usage_stats?.total_savings || {}) as Record<
|
|
197
|
+
string,
|
|
198
|
+
{ formattedAmount: string }
|
|
199
|
+
>;
|
|
200
|
+
|
|
201
|
+
// If no savings data, return dash
|
|
202
|
+
if (Object.keys(totalSavingsByCurrency).length === 0) {
|
|
203
|
+
return <Typography variant="body2">—</Typography>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Display savings by currency
|
|
207
|
+
return (
|
|
208
|
+
<Box>
|
|
209
|
+
{Object.entries(totalSavingsByCurrency).map(([currencyId, value], i) => {
|
|
210
|
+
if (!value) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
<Typography key={currencyId} variant="body2" sx={{ display: 'inline-flex' }}>
|
|
215
|
+
{i > 0 && '、'}
|
|
216
|
+
{value.formattedAmount}
|
|
217
|
+
</Typography>
|
|
218
|
+
);
|
|
219
|
+
})}
|
|
220
|
+
</Box>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
label: t('admin.coupon.usagePeriod'),
|
|
227
|
+
name: 'usage_period',
|
|
228
|
+
options: {
|
|
229
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
230
|
+
const customer = data.customers?.[index];
|
|
231
|
+
const stats = customer?.coupon_usage_stats;
|
|
232
|
+
if (!stats) return '—';
|
|
233
|
+
return (
|
|
234
|
+
<Box>
|
|
235
|
+
<Typography variant="body2">
|
|
236
|
+
{t('admin.coupon.firstUsed')}: {formatTime(stats.first_used)}
|
|
237
|
+
</Typography>
|
|
238
|
+
{stats.first_used !== stats.last_used && (
|
|
239
|
+
<Typography
|
|
240
|
+
variant="body2"
|
|
241
|
+
sx={{
|
|
242
|
+
color: 'text.secondary',
|
|
243
|
+
}}>
|
|
244
|
+
{t('admin.coupon.lastUsed')}: {formatTime(stats.last_used)}
|
|
245
|
+
</Typography>
|
|
246
|
+
)}
|
|
247
|
+
</Box>
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return baseColumns;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const customerColumns = buildCustomerColumns();
|
|
258
|
+
|
|
259
|
+
// Build subscription columns dynamically based on entity type
|
|
260
|
+
const buildSubscriptionColumns = () => {
|
|
261
|
+
const baseColumns = [
|
|
262
|
+
{
|
|
263
|
+
label: t('admin.subscription.product'),
|
|
264
|
+
name: 'id',
|
|
265
|
+
options: {
|
|
266
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
267
|
+
const subscription = data.subscriptions?.[index];
|
|
268
|
+
return (
|
|
269
|
+
<Link to={`/admin/billing/${subscription.id}`}>
|
|
270
|
+
<SubscriptionDescription subscription={subscription} />
|
|
271
|
+
</Link>
|
|
272
|
+
);
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
label: t('common.status'),
|
|
278
|
+
name: 'status',
|
|
279
|
+
options: {
|
|
280
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
281
|
+
const subscription = data.subscriptions?.[index];
|
|
282
|
+
return (
|
|
283
|
+
<Link to={`/admin/billing/${subscription.id}`}>
|
|
284
|
+
<SubscriptionStatus subscription={subscription} />
|
|
285
|
+
</Link>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
label: t('common.customer'),
|
|
292
|
+
name: 'customer_info',
|
|
293
|
+
options: {
|
|
294
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
295
|
+
const subscription = data.subscriptions?.[index];
|
|
296
|
+
const customer = subscription?.customer;
|
|
297
|
+
return <CustomerLink customer={customer} size="small" />;
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
// Only show promotion codes column when in coupon mode
|
|
304
|
+
if (entityType === 'coupon') {
|
|
305
|
+
baseColumns.push({
|
|
306
|
+
label: t('admin.coupon.promotionCodesUsed'),
|
|
307
|
+
name: 'promotion_code_info',
|
|
308
|
+
options: {
|
|
309
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
310
|
+
const subscription = data.subscriptions?.[index];
|
|
311
|
+
const promoCode = subscription?.discount_info?.promotion_code;
|
|
312
|
+
if (!promoCode) {
|
|
313
|
+
return (
|
|
314
|
+
<Typography
|
|
315
|
+
variant="body2"
|
|
316
|
+
sx={{
|
|
317
|
+
color: 'text.secondary',
|
|
318
|
+
}}>
|
|
319
|
+
{t('admin.coupon.directCouponUsage')}
|
|
320
|
+
</Typography>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
return (
|
|
324
|
+
<Box>
|
|
325
|
+
<Chip
|
|
326
|
+
label={promoCode.code}
|
|
327
|
+
size="small"
|
|
328
|
+
variant="outlined"
|
|
329
|
+
onClick={() => navigate(`/admin/products/promotion-codes/${promoCode.id}`)}
|
|
330
|
+
sx={{
|
|
331
|
+
cursor: 'pointer',
|
|
332
|
+
mb: 0.5,
|
|
333
|
+
'&:hover': {
|
|
334
|
+
backgroundColor: 'action.hover',
|
|
335
|
+
},
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
</Box>
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Add total savings, discount info, and created columns
|
|
346
|
+
baseColumns.push(
|
|
347
|
+
{
|
|
348
|
+
label: t('admin.coupon.totalSavings'),
|
|
349
|
+
name: 'total_savings',
|
|
350
|
+
options: {
|
|
351
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
352
|
+
const subscription = data.subscriptions?.[index];
|
|
353
|
+
const totalSavingsByCurrency = (subscription?.discount_info?.total_savings || {}) as Record<
|
|
354
|
+
string,
|
|
355
|
+
{ formattedAmount: string }
|
|
356
|
+
>;
|
|
357
|
+
|
|
358
|
+
// If no savings data, return dash
|
|
359
|
+
if (Object.keys(totalSavingsByCurrency).length === 0) {
|
|
360
|
+
return (
|
|
361
|
+
<Typography variant="body2" sx={{ color: 'text.lighter' }}>
|
|
362
|
+
—
|
|
363
|
+
</Typography>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Display savings by currency
|
|
368
|
+
return (
|
|
369
|
+
<Box>
|
|
370
|
+
{Object.entries(totalSavingsByCurrency).map(([currencyId, value], i) => {
|
|
371
|
+
if (!value) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
return (
|
|
375
|
+
<Typography key={currencyId} variant="body2" sx={{ lineHeight: 1.2 }}>
|
|
376
|
+
{i > 0 && ', '}
|
|
377
|
+
{value.formattedAmount}
|
|
378
|
+
</Typography>
|
|
379
|
+
);
|
|
380
|
+
})}
|
|
381
|
+
</Box>
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
label: t('admin.coupon.discountInfo'),
|
|
388
|
+
name: 'discount_info',
|
|
389
|
+
options: {
|
|
390
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
391
|
+
const subscription = data.subscriptions?.[index];
|
|
392
|
+
const discountInfo = subscription?.discount_info;
|
|
393
|
+
if (!discountInfo) return <Typography variant="body2">—</Typography>;
|
|
394
|
+
|
|
395
|
+
const couponInfo = discountInfo.coupon_info;
|
|
396
|
+
return (
|
|
397
|
+
<Box>
|
|
398
|
+
<Typography variant="body2">
|
|
399
|
+
{t('admin.coupon.discountPeriod')}: {formatTime(discountInfo.discount_start * 1000)}
|
|
400
|
+
{discountInfo.discount_end && ` - ${formatTime(discountInfo.discount_end * 1000)}`}
|
|
401
|
+
</Typography>
|
|
402
|
+
<Typography
|
|
403
|
+
variant="caption"
|
|
404
|
+
sx={{
|
|
405
|
+
color: 'text.secondary',
|
|
406
|
+
}}>
|
|
407
|
+
{t('admin.coupon.duration')}:{' '}
|
|
408
|
+
{t(`admin.coupon.couponTermsDuration.${couponInfo.duration}`, {
|
|
409
|
+
months: couponInfo.duration_in_months,
|
|
410
|
+
})}
|
|
411
|
+
</Typography>
|
|
412
|
+
</Box>
|
|
413
|
+
);
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
label: t('common.created'),
|
|
419
|
+
name: 'created_at',
|
|
420
|
+
options: {
|
|
421
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
422
|
+
const subscription = data.subscriptions?.[index];
|
|
423
|
+
return formatTime(subscription?.created_at);
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return baseColumns;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const subscriptionColumns = buildSubscriptionColumns();
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<Box>
|
|
436
|
+
<Tabs
|
|
437
|
+
value={tabValue}
|
|
438
|
+
onChange={handleTabChange}
|
|
439
|
+
sx={{
|
|
440
|
+
flex: '1 0 auto',
|
|
441
|
+
maxWidth: '100%',
|
|
442
|
+
fontSize: 14,
|
|
443
|
+
borderBottom: 1,
|
|
444
|
+
borderColor: 'divider',
|
|
445
|
+
'.Mui-selected': {
|
|
446
|
+
fontSize: '14px !important',
|
|
447
|
+
color: 'primary.main',
|
|
448
|
+
},
|
|
449
|
+
}}>
|
|
450
|
+
<Tab label={t('admin.coupon.customers')} />
|
|
451
|
+
<Tab label={t('admin.coupon.subscriptions')} />
|
|
452
|
+
</Tabs>
|
|
453
|
+
{tabValue === 0 && (
|
|
454
|
+
<Box sx={{ mt: 2 }}>
|
|
455
|
+
{data.customers?.length > 0 ? (
|
|
456
|
+
<Table
|
|
457
|
+
data={data.customers}
|
|
458
|
+
columns={customerColumns}
|
|
459
|
+
loading={loading}
|
|
460
|
+
toolbar={false}
|
|
461
|
+
options={{
|
|
462
|
+
selectableRows: 'none',
|
|
463
|
+
serverSide: true,
|
|
464
|
+
count: data.count || 0,
|
|
465
|
+
page: customerPagination.page,
|
|
466
|
+
rowsPerPage: customerPagination.pageSize,
|
|
467
|
+
rowsPerPageOptions: [5, 10, 25, 50],
|
|
468
|
+
}}
|
|
469
|
+
onChange={handleCustomerTableChange}
|
|
470
|
+
/>
|
|
471
|
+
) : (
|
|
472
|
+
!loading && (
|
|
473
|
+
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
474
|
+
<Typography
|
|
475
|
+
variant="h6"
|
|
476
|
+
sx={{
|
|
477
|
+
color: 'text.secondary',
|
|
478
|
+
}}>
|
|
479
|
+
{t('admin.coupon.noCustomersFound')}
|
|
480
|
+
</Typography>
|
|
481
|
+
<Typography
|
|
482
|
+
variant="body2"
|
|
483
|
+
sx={{
|
|
484
|
+
color: 'text.secondary',
|
|
485
|
+
}}>
|
|
486
|
+
{t('admin.coupon.noCustomersRedeem')}
|
|
487
|
+
</Typography>
|
|
488
|
+
</Box>
|
|
489
|
+
)
|
|
490
|
+
)}
|
|
491
|
+
</Box>
|
|
492
|
+
)}
|
|
493
|
+
{tabValue === 1 && (
|
|
494
|
+
<Box>
|
|
495
|
+
{data.subscriptions?.length > 0 ? (
|
|
496
|
+
<Table
|
|
497
|
+
data={data.subscriptions}
|
|
498
|
+
columns={subscriptionColumns}
|
|
499
|
+
loading={loading}
|
|
500
|
+
options={{
|
|
501
|
+
selectableRows: 'none',
|
|
502
|
+
serverSide: true,
|
|
503
|
+
count: data.count || 0,
|
|
504
|
+
page: subscriptionPagination.page,
|
|
505
|
+
rowsPerPage: subscriptionPagination.pageSize,
|
|
506
|
+
rowsPerPageOptions: [5, 10, 25, 50],
|
|
507
|
+
}}
|
|
508
|
+
onChange={handleSubscriptionTableChange}
|
|
509
|
+
/>
|
|
510
|
+
) : (
|
|
511
|
+
!loading && (
|
|
512
|
+
<Box sx={{ textAlign: 'center', py: 4 }}>
|
|
513
|
+
<Typography
|
|
514
|
+
variant="h6"
|
|
515
|
+
sx={{
|
|
516
|
+
color: 'text.secondary',
|
|
517
|
+
}}>
|
|
518
|
+
{t('admin.coupon.noSubscriptionsFound')}
|
|
519
|
+
</Typography>
|
|
520
|
+
<Typography
|
|
521
|
+
variant="body2"
|
|
522
|
+
sx={{
|
|
523
|
+
color: 'text.secondary',
|
|
524
|
+
}}>
|
|
525
|
+
{t('admin.coupon.noSubscriptionsRedeem')}
|
|
526
|
+
</Typography>
|
|
527
|
+
</Box>
|
|
528
|
+
)
|
|
529
|
+
)}
|
|
530
|
+
</Box>
|
|
531
|
+
)}
|
|
532
|
+
</Box>
|
|
533
|
+
);
|
|
534
|
+
}
|