payment-kit 1.20.13 → 1.20.15
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/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +2 -3
- package/api/src/libs/vendor-util/fulfillment.ts +16 -30
- package/api/src/queues/vendors/commission.ts +32 -42
- package/api/src/queues/vendors/fulfillment-coordinator.ts +68 -60
- package/api/src/queues/vendors/fulfillment.ts +5 -5
- package/api/src/queues/vendors/return-processor.ts +0 -1
- package/api/src/queues/vendors/status-check.ts +2 -2
- package/api/src/routes/checkout-sessions.ts +15 -2
- package/api/src/routes/coupons.ts +7 -0
- package/api/src/routes/credit-grants.ts +8 -1
- package/api/src/routes/credit-transactions.ts +153 -13
- package/api/src/routes/invoices.ts +35 -1
- package/api/src/routes/meter-events.ts +31 -3
- package/api/src/routes/meters.ts +4 -0
- package/api/src/routes/payment-currencies.ts +2 -1
- package/api/src/routes/promotion-codes.ts +2 -2
- package/api/src/routes/subscription-items.ts +4 -0
- package/api/src/routes/vendor.ts +13 -4
- package/api/src/routes/webhook-endpoints.ts +4 -0
- package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
- package/api/src/store/models/checkout-session.ts +23 -0
- package/api/src/store/models/credit-transaction.ts +5 -0
- package/api/src/store/models/meter-event.ts +22 -12
- package/api/src/store/models/types.ts +18 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/customer/related-credit-grants.tsx +194 -0
- package/src/components/meter/add-usage-dialog.tsx +8 -0
- package/src/components/meter/events-list.tsx +93 -96
- package/src/components/product/form.tsx +0 -1
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -2
- package/src/pages/customer/invoice/detail.tsx +11 -2
- package/doc/vendor_fulfillment_system.md +0 -929
|
@@ -135,6 +135,11 @@ export class CreditTransaction extends Model<
|
|
|
135
135
|
foreignKey: 'subscription_id',
|
|
136
136
|
as: 'subscription',
|
|
137
137
|
});
|
|
138
|
+
|
|
139
|
+
this.belongsTo(models.MeterEvent, {
|
|
140
|
+
foreignKey: 'source',
|
|
141
|
+
as: 'meterEvent',
|
|
142
|
+
});
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
public static async getUsageSummary({
|
|
@@ -14,7 +14,7 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
14
14
|
import { BN } from '@ocap/util';
|
|
15
15
|
import { createEvent } from '../../libs/audit';
|
|
16
16
|
import { createIdGenerator } from '../../libs/util';
|
|
17
|
-
import { GroupedBN, GroupedStrList, MeterEventPayload, MeterEventStatus } from './types';
|
|
17
|
+
import { GroupedBN, GroupedStrList, MeterEventPayload, MeterEventStatus, SourceData } from './types';
|
|
18
18
|
import { Customer } from './customer';
|
|
19
19
|
import { Subscription, type TSubscription } from './subscription';
|
|
20
20
|
import { Meter, type TMeter } from './meter';
|
|
@@ -43,6 +43,7 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
43
43
|
declare credit_consumed: string; // 已消费的credit数量
|
|
44
44
|
declare credit_pending: string; // 待消费的credit数量(债务)
|
|
45
45
|
declare metadata?: Record<string, any>;
|
|
46
|
+
declare source_data?: SourceData;
|
|
46
47
|
|
|
47
48
|
// 审计字段
|
|
48
49
|
declare created_by?: string;
|
|
@@ -215,18 +216,27 @@ export class MeterEvent extends Model<InferAttributes<MeterEvent>, InferCreation
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
public static initialize(sequelize: any) {
|
|
218
|
-
this.init(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
hooks: {
|
|
226
|
-
afterCreate: (model: MeterEvent, options) =>
|
|
227
|
-
createEvent('MeterEvent', 'billing.meter_event.created', model, options).catch(console.error),
|
|
219
|
+
this.init(
|
|
220
|
+
{
|
|
221
|
+
...this.GENESIS_ATTRIBUTES,
|
|
222
|
+
source_data: {
|
|
223
|
+
type: DataTypes.JSON,
|
|
224
|
+
allowNull: true,
|
|
225
|
+
},
|
|
228
226
|
},
|
|
229
|
-
|
|
227
|
+
{
|
|
228
|
+
sequelize,
|
|
229
|
+
modelName: 'MeterEvent',
|
|
230
|
+
tableName: 'meter_events',
|
|
231
|
+
createdAt: 'created_at',
|
|
232
|
+
updatedAt: 'updated_at',
|
|
233
|
+
indexes: [{ fields: ['identifier'], unique: true }, { fields: ['status'] }, { fields: ['event_name'] }],
|
|
234
|
+
hooks: {
|
|
235
|
+
afterCreate: (model: MeterEvent, options) =>
|
|
236
|
+
createEvent('MeterEvent', 'billing.meter_event.created', model, options).catch(console.error),
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
);
|
|
230
240
|
}
|
|
231
241
|
|
|
232
242
|
// 批量处理未处理的事件
|
|
@@ -817,3 +817,21 @@ export type Restrictions = {
|
|
|
817
817
|
minimum_amount?: string;
|
|
818
818
|
minimum_amount_currency?: string;
|
|
819
819
|
};
|
|
820
|
+
|
|
821
|
+
export type LocalizedText = {
|
|
822
|
+
zh: string;
|
|
823
|
+
en: string;
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
export type SimpleSourceData = Record<string, string>;
|
|
827
|
+
|
|
828
|
+
export type StructuredSourceDataField = {
|
|
829
|
+
key: string;
|
|
830
|
+
label: string | LocalizedText;
|
|
831
|
+
value: string;
|
|
832
|
+
type?: 'text' | 'image' | 'url';
|
|
833
|
+
url?: string;
|
|
834
|
+
group?: string;
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
export type SourceData = SimpleSourceData | StructuredSourceDataField[];
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.15",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
58
58
|
"@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
59
|
-
"@blocklet/payment-react": "1.20.
|
|
60
|
-
"@blocklet/payment-vendor": "1.20.
|
|
59
|
+
"@blocklet/payment-react": "1.20.15",
|
|
60
|
+
"@blocklet/payment-vendor": "1.20.15",
|
|
61
61
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
62
62
|
"@blocklet/ui-react": "^3.1.41",
|
|
63
63
|
"@blocklet/uploader": "^0.2.11",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"devDependencies": {
|
|
127
127
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
128
128
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
129
|
-
"@blocklet/payment-types": "1.20.
|
|
129
|
+
"@blocklet/payment-types": "1.20.15",
|
|
130
130
|
"@types/cookie-parser": "^1.4.9",
|
|
131
131
|
"@types/cors": "^2.8.19",
|
|
132
132
|
"@types/debug": "^4.1.12",
|
|
@@ -173,5 +173,5 @@
|
|
|
173
173
|
"parser": "typescript"
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "d205c3b1ec7d2b819e375ed2eb8b70c9d48f0bcb"
|
|
177
177
|
}
|
|
@@ -324,7 +324,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
324
324
|
)}
|
|
325
325
|
{creditTab === CreditTab.GRANTS && <CreditGrantsList customer_id={customerId} mode={mode} key={creditTab} />}
|
|
326
326
|
{creditTab === CreditTab.TRANSACTIONS && (
|
|
327
|
-
<CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} />
|
|
327
|
+
<CreditTransactionsList customer_id={customerId} mode={mode} key={creditTab} includeGrants />
|
|
328
328
|
)}
|
|
329
329
|
</Box>
|
|
330
330
|
{autoRecharge.open && (
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { formatBNStr, formatToDate, Table, usePaymentContext } from '@blocklet/payment-react';
|
|
4
|
+
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
5
|
+
import { Box, Chip, Divider, styled, Typography } from '@mui/material';
|
|
6
|
+
import { useNavigate } from 'react-router-dom';
|
|
7
|
+
|
|
8
|
+
interface RelatedCreditGrantsProps {
|
|
9
|
+
grants: TCreditGrantExpanded[];
|
|
10
|
+
showDivider?: boolean;
|
|
11
|
+
mode?: 'dashboard' | 'portal';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusChip({ status, label = '' }: { status: string; label?: string }) {
|
|
15
|
+
const getStatusColor = (statusValue: string) => {
|
|
16
|
+
switch (statusValue) {
|
|
17
|
+
case 'granted':
|
|
18
|
+
return 'success';
|
|
19
|
+
case 'pending':
|
|
20
|
+
return 'warning';
|
|
21
|
+
case 'expired':
|
|
22
|
+
return 'default';
|
|
23
|
+
case 'depleted':
|
|
24
|
+
return 'default';
|
|
25
|
+
case 'voided':
|
|
26
|
+
return 'default';
|
|
27
|
+
default:
|
|
28
|
+
return 'default';
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return <Chip label={label || status} size="small" color={getStatusColor(status) as any} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function RelatedCreditGrants({ grants, showDivider = true, mode = 'portal' }: RelatedCreditGrantsProps) {
|
|
36
|
+
const { t, locale } = useLocaleContext();
|
|
37
|
+
const { session } = usePaymentContext();
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
|
|
40
|
+
const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
|
|
41
|
+
|
|
42
|
+
const inDashboard = mode === 'dashboard' && isAdmin;
|
|
43
|
+
|
|
44
|
+
if (!grants?.length) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleShowGrantDetail = (grant: TCreditGrantExpanded) => {
|
|
49
|
+
let path = `/customer/credit-grant/${grant.id}`;
|
|
50
|
+
if (inDashboard) {
|
|
51
|
+
path = `/admin/customers/${grant.id}`;
|
|
52
|
+
}
|
|
53
|
+
navigate(path);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const columns = [
|
|
57
|
+
{
|
|
58
|
+
label: t('common.name'),
|
|
59
|
+
name: 'name',
|
|
60
|
+
options: {
|
|
61
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
62
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
63
|
+
return <Box onClick={() => handleShowGrantDetail(grant)}>{grant.name || grant.id}</Box>;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: t('common.status'),
|
|
69
|
+
name: 'status',
|
|
70
|
+
options: {
|
|
71
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
72
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
73
|
+
return (
|
|
74
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
75
|
+
<StatusChip status={grant.status} label={t(`admin.customer.creditGrants.status.${grant.status}`)} />
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: t('common.remainingCredit'),
|
|
83
|
+
name: 'remaining_amount',
|
|
84
|
+
align: 'right',
|
|
85
|
+
options: {
|
|
86
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
87
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
88
|
+
return (
|
|
89
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
90
|
+
<Typography variant="body2">
|
|
91
|
+
{formatBNStr(grant.remaining_amount, grant.paymentCurrency?.decimal || 0)}{' '}
|
|
92
|
+
{grant.paymentCurrency?.symbol}
|
|
93
|
+
</Typography>
|
|
94
|
+
</Box>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: t('common.scope'),
|
|
101
|
+
name: 'scope',
|
|
102
|
+
options: {
|
|
103
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
104
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
105
|
+
let scope = 'general';
|
|
106
|
+
if (grant.applicability_config?.scope?.prices) {
|
|
107
|
+
scope = 'specific';
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
111
|
+
{scope === 'specific' ? t('common.specific') : t('common.general')}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: t('common.effectiveDate'),
|
|
119
|
+
name: 'effective_at',
|
|
120
|
+
options: {
|
|
121
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
122
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
123
|
+
const effectiveAt = grant.effective_at ? grant.effective_at * 1000 : grant.created_at;
|
|
124
|
+
return (
|
|
125
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
126
|
+
{formatToDate(effectiveAt, locale, 'YYYY-MM-DD HH:mm')}
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: t('common.expirationDate'),
|
|
134
|
+
name: 'expires_at',
|
|
135
|
+
options: {
|
|
136
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
137
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
138
|
+
return (
|
|
139
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
140
|
+
<Typography variant="body2">
|
|
141
|
+
{grant.expires_at ? formatToDate(grant.expires_at * 1000, locale, 'YYYY-MM-DD HH:mm') : '-'}
|
|
142
|
+
</Typography>
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
{showDivider && <Divider />}
|
|
153
|
+
<TableRoot className="section">
|
|
154
|
+
<Typography
|
|
155
|
+
variant="h3"
|
|
156
|
+
className="section-header"
|
|
157
|
+
sx={{
|
|
158
|
+
mb: 2,
|
|
159
|
+
}}>
|
|
160
|
+
{t('admin.customer.creditGrants.relatedGrants')}
|
|
161
|
+
</Typography>
|
|
162
|
+
<Table
|
|
163
|
+
data={grants}
|
|
164
|
+
columns={columns}
|
|
165
|
+
options={{
|
|
166
|
+
count: grants.length,
|
|
167
|
+
page: 0,
|
|
168
|
+
rowsPerPage: grants.length,
|
|
169
|
+
pagination: false,
|
|
170
|
+
}}
|
|
171
|
+
loading={false}
|
|
172
|
+
toolbar={false}
|
|
173
|
+
footer={false}
|
|
174
|
+
emptyNodeText={t('admin.customer.creditGrants.noGrants')}
|
|
175
|
+
/>
|
|
176
|
+
</TableRoot>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const TableRoot = styled(Box)`
|
|
182
|
+
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
183
|
+
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
|
|
184
|
+
> div {
|
|
185
|
+
width: fit-content;
|
|
186
|
+
flex: inherit;
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
.invoice-summary {
|
|
191
|
+
padding-right: 20px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
@@ -41,6 +41,14 @@ const addMeterEvent = (data: any): Promise<any> => {
|
|
|
41
41
|
source: 'manual',
|
|
42
42
|
created_by: 'admin',
|
|
43
43
|
},
|
|
44
|
+
source_data: [
|
|
45
|
+
{
|
|
46
|
+
key: 'origin',
|
|
47
|
+
label: 'created_by',
|
|
48
|
+
value: 'Admin',
|
|
49
|
+
type: 'text',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
44
52
|
})
|
|
45
53
|
.then((res) => res.data);
|
|
46
54
|
};
|
|
@@ -5,7 +5,6 @@ import type { TCustomer, TPaymentCurrency, TMeterEventExpanded } from '@blocklet
|
|
|
5
5
|
import {
|
|
6
6
|
Box,
|
|
7
7
|
Typography,
|
|
8
|
-
Alert,
|
|
9
8
|
Stack,
|
|
10
9
|
Card,
|
|
11
10
|
Autocomplete,
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
} from '@mui/material';
|
|
20
19
|
import { CalendarTodayOutlined, Add } from '@mui/icons-material';
|
|
21
20
|
import { useEffect, useMemo } from 'react';
|
|
22
|
-
import { useSetState } from 'ahooks';
|
|
21
|
+
import { useSetState, useLocalStorageState, useRequest } from 'ahooks';
|
|
23
22
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
24
23
|
import { fromUnitToToken } from '@ocap/util';
|
|
25
24
|
import { Link } from 'react-router-dom';
|
|
@@ -34,6 +33,14 @@ interface MeterEventsListProps {
|
|
|
34
33
|
paymentCurrency: TPaymentCurrency;
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
type SearchProps = {
|
|
37
|
+
pageSize: number;
|
|
38
|
+
page: number;
|
|
39
|
+
customer_id?: string;
|
|
40
|
+
start?: number;
|
|
41
|
+
end?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
37
44
|
interface TMeterEventStats {
|
|
38
45
|
date: string;
|
|
39
46
|
event_count: number;
|
|
@@ -42,22 +49,9 @@ interface TMeterEventStats {
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
interface EventsState {
|
|
45
|
-
events: TMeterEventExpanded[];
|
|
46
52
|
stats: TMeterEventStats[];
|
|
47
53
|
customers: TCustomer[];
|
|
48
|
-
loading: boolean;
|
|
49
54
|
statsLoading: boolean;
|
|
50
|
-
error: string | null;
|
|
51
|
-
pagination: {
|
|
52
|
-
page: number;
|
|
53
|
-
limit: number;
|
|
54
|
-
total: number;
|
|
55
|
-
};
|
|
56
|
-
filters: {
|
|
57
|
-
customer_id?: string;
|
|
58
|
-
start?: string;
|
|
59
|
-
end?: string;
|
|
60
|
-
};
|
|
61
55
|
anchorEl: any;
|
|
62
56
|
startDate: Date;
|
|
63
57
|
endDate: Date;
|
|
@@ -66,14 +60,30 @@ interface EventsState {
|
|
|
66
60
|
addUsageDialog: boolean;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
const fetchEvents = (
|
|
63
|
+
const fetchEvents = (
|
|
64
|
+
meterId: string,
|
|
65
|
+
params: SearchProps = {} as SearchProps
|
|
66
|
+
): Promise<{ list: TMeterEventExpanded[]; count: number }> => {
|
|
70
67
|
const searchParams = new URLSearchParams();
|
|
71
68
|
searchParams.append('meter_id', meterId);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
69
|
+
|
|
70
|
+
// 确保 start 和 end 参数被正确添加
|
|
71
|
+
if (params.page !== undefined) {
|
|
72
|
+
searchParams.append('page', String(params.page));
|
|
73
|
+
}
|
|
74
|
+
if (params.pageSize !== undefined) {
|
|
75
|
+
searchParams.append('pageSize', String(params.pageSize));
|
|
76
|
+
}
|
|
77
|
+
if (params.start !== undefined) {
|
|
78
|
+
searchParams.append('start', String(params.start));
|
|
79
|
+
}
|
|
80
|
+
if (params.end !== undefined) {
|
|
81
|
+
searchParams.append('end', String(params.end));
|
|
82
|
+
}
|
|
83
|
+
if (params.customer_id) {
|
|
84
|
+
searchParams.append('customer_id', params.customer_id);
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
return api.get(`/api/meter-events?${searchParams.toString()}`).then((res: any) => res.data);
|
|
78
88
|
};
|
|
79
89
|
|
|
@@ -143,19 +153,31 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
143
153
|
const { isMobile } = useMobile('md');
|
|
144
154
|
const { livemode } = usePaymentContext();
|
|
145
155
|
const maxDate = dayjs().endOf('day').toDate();
|
|
156
|
+
|
|
157
|
+
const [search, setSearch] = useLocalStorageState<SearchProps>(`meter-events-${meterId}`, {
|
|
158
|
+
defaultValue: {
|
|
159
|
+
pageSize: 10,
|
|
160
|
+
page: 1,
|
|
161
|
+
start: dayjs().subtract(30, 'day').startOf('day').unix(),
|
|
162
|
+
end: dayjs().endOf('day').unix(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const {
|
|
167
|
+
data = {
|
|
168
|
+
list: [],
|
|
169
|
+
count: 0,
|
|
170
|
+
},
|
|
171
|
+
refresh,
|
|
172
|
+
loading,
|
|
173
|
+
} = useRequest(() => fetchEvents(meterId, search), {
|
|
174
|
+
refreshDeps: [search],
|
|
175
|
+
});
|
|
176
|
+
|
|
146
177
|
const [state, setState] = useSetState<EventsState>({
|
|
147
|
-
events: [],
|
|
148
178
|
stats: [],
|
|
149
179
|
customers: [],
|
|
150
|
-
loading: false,
|
|
151
180
|
statsLoading: false,
|
|
152
|
-
error: null,
|
|
153
|
-
pagination: {
|
|
154
|
-
page: 0,
|
|
155
|
-
limit: 10,
|
|
156
|
-
total: 0,
|
|
157
|
-
},
|
|
158
|
-
filters: {},
|
|
159
181
|
anchorEl: null,
|
|
160
182
|
startDate: dayjs().subtract(30, 'day').startOf('day').toDate(),
|
|
161
183
|
endDate: maxDate,
|
|
@@ -209,35 +231,6 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
209
231
|
return emptyData.map((empty) => dataMap.get(empty.date) || empty);
|
|
210
232
|
}, [state.stats, state.statsLoading, state.startDate, state.endDate, granularity]);
|
|
211
233
|
|
|
212
|
-
const loadEvents = async (page = 0, limit = 10) => {
|
|
213
|
-
setState({ loading: true, error: null });
|
|
214
|
-
try {
|
|
215
|
-
const params: any = {
|
|
216
|
-
page,
|
|
217
|
-
limit,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
if (state.startDate && state.endDate) {
|
|
221
|
-
params.start = dayjs(state.startDate).unix();
|
|
222
|
-
params.end = dayjs(state.endDate).unix();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (state.selectedCustomer) {
|
|
226
|
-
params.customer_id = state.selectedCustomer.id;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const result = await fetchEvents(meterId, params);
|
|
230
|
-
setState({
|
|
231
|
-
events: result.list,
|
|
232
|
-
pagination: { page, limit, total: result.count },
|
|
233
|
-
loading: false,
|
|
234
|
-
});
|
|
235
|
-
} catch (err) {
|
|
236
|
-
console.error('Failed to fetch events:', err);
|
|
237
|
-
setState({ error: 'Failed to load events', loading: false });
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
234
|
const loadStats = async () => {
|
|
242
235
|
setState({ statsLoading: true });
|
|
243
236
|
try {
|
|
@@ -259,28 +252,43 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
259
252
|
try {
|
|
260
253
|
const result = await fetchCustomers();
|
|
261
254
|
setState({ customers: result.list });
|
|
255
|
+
|
|
256
|
+
// Initialize selected customer if exists in search
|
|
257
|
+
if (search!.customer_id && result.list.length > 0) {
|
|
258
|
+
const selectedCustomer = result.list.find((c) => c.id === search!.customer_id);
|
|
259
|
+
if (selectedCustomer) {
|
|
260
|
+
setState({ selectedCustomer });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
262
263
|
} catch (err) {
|
|
263
264
|
console.error('Failed to fetch customers:', err);
|
|
264
265
|
}
|
|
265
266
|
};
|
|
266
267
|
|
|
267
268
|
useEffect(() => {
|
|
268
|
-
loadEvents();
|
|
269
|
-
loadStats();
|
|
270
269
|
loadCustomers();
|
|
271
270
|
}, [meterId]);
|
|
272
271
|
|
|
272
|
+
// Initialize state dates from search params
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (search?.start && search?.end) {
|
|
275
|
+
setState({
|
|
276
|
+
startDate: dayjs.unix(search.start).toDate(),
|
|
277
|
+
endDate: dayjs.unix(search.end).toDate(),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}, [search?.start, search?.end]);
|
|
281
|
+
|
|
273
282
|
useEffect(() => {
|
|
274
|
-
loadEvents(0, state.pagination.limit);
|
|
275
283
|
loadStats();
|
|
276
284
|
}, [state.startDate, state.endDate, state.selectedCustomer, granularity]);
|
|
277
285
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
const onTableChange = ({ page, rowsPerPage }: any) => {
|
|
287
|
+
if (search!.pageSize !== rowsPerPage) {
|
|
288
|
+
setSearch((x) => ({ ...x!, pageSize: rowsPerPage, page: 1 }));
|
|
289
|
+
} else if (search!.page !== page + 1) {
|
|
290
|
+
setSearch((x) => ({ ...x!, page: page + 1 }));
|
|
291
|
+
}
|
|
284
292
|
};
|
|
285
293
|
|
|
286
294
|
const onTogglePicker = (e: any) => {
|
|
@@ -292,6 +300,10 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
292
300
|
};
|
|
293
301
|
|
|
294
302
|
const onRangeChange = (range: any) => {
|
|
303
|
+
const newStart = dayjs(range.startDate).startOf('day').unix();
|
|
304
|
+
const newEnd = dayjs(range.endDate).endOf('day').unix();
|
|
305
|
+
|
|
306
|
+
setSearch((x) => ({ ...x!, start: newStart, end: newEnd, page: 1 }));
|
|
295
307
|
setState({
|
|
296
308
|
startDate: range.startDate,
|
|
297
309
|
endDate: dayjs(range.endDate).endOf('day').toDate(),
|
|
@@ -306,6 +318,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
306
318
|
};
|
|
307
319
|
|
|
308
320
|
const handleCustomerChange = (customer: TCustomer | null) => {
|
|
321
|
+
setSearch((x) => ({ ...x!, customer_id: customer?.id, page: 1 }));
|
|
309
322
|
setState({ selectedCustomer: customer });
|
|
310
323
|
};
|
|
311
324
|
|
|
@@ -322,18 +335,10 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
322
335
|
};
|
|
323
336
|
|
|
324
337
|
const handleAddUsageSuccess = () => {
|
|
325
|
-
|
|
338
|
+
refresh();
|
|
326
339
|
loadStats();
|
|
327
340
|
};
|
|
328
341
|
|
|
329
|
-
if (state.error) {
|
|
330
|
-
return (
|
|
331
|
-
<Alert severity="error" sx={{ mt: 1 }}>
|
|
332
|
-
{state.error}
|
|
333
|
-
</Alert>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
342
|
const open = Boolean(state.anchorEl);
|
|
338
343
|
const id = open ? 'date-range-picker-popover' : undefined;
|
|
339
344
|
|
|
@@ -345,7 +350,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
345
350
|
filter: false,
|
|
346
351
|
sort: false,
|
|
347
352
|
customBodyRenderLite: (_: string, index: number) => {
|
|
348
|
-
const item =
|
|
353
|
+
const item = data.list[index];
|
|
349
354
|
if (!item) return null;
|
|
350
355
|
const value = fromUnitToToken(item.payload.value || '0', paymentCurrency.decimal);
|
|
351
356
|
return <Link to={`/admin/billing/${item.id}`}>{`${value} ${paymentCurrency.symbol}`}</Link>;
|
|
@@ -359,7 +364,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
359
364
|
filter: false,
|
|
360
365
|
sort: false,
|
|
361
366
|
customBodyRenderLite: (_: string, index: number) => {
|
|
362
|
-
const item =
|
|
367
|
+
const item = data.list[index];
|
|
363
368
|
if (!item || !item?.customer) return '-';
|
|
364
369
|
return <CustomerLink customer={item.customer} size="small" />;
|
|
365
370
|
},
|
|
@@ -372,7 +377,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
372
377
|
filter: false,
|
|
373
378
|
sort: false,
|
|
374
379
|
customBodyRenderLite: (_: string, index: number) => {
|
|
375
|
-
const item =
|
|
380
|
+
const item = data.list[index];
|
|
376
381
|
if (!item) return '-';
|
|
377
382
|
const subscriptionId = (item as any).payload?.subscription_id;
|
|
378
383
|
if (!subscriptionId) return '-';
|
|
@@ -393,7 +398,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
393
398
|
filter: false,
|
|
394
399
|
sort: false,
|
|
395
400
|
customBodyRenderLite: (_: string, index: number) => {
|
|
396
|
-
const item =
|
|
401
|
+
const item = data.list[index];
|
|
397
402
|
return <Link to={`/admin/billing/${item?.id}`}>{item ? formatTime(item.created_at) : '-'}</Link>;
|
|
398
403
|
},
|
|
399
404
|
},
|
|
@@ -610,28 +615,20 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
610
615
|
sx={{
|
|
611
616
|
color: 'text.secondary',
|
|
612
617
|
}}>
|
|
613
|
-
{t('admin.meter.events.count', { count:
|
|
618
|
+
{t('admin.meter.events.count', { count: data.count })}
|
|
614
619
|
</Typography>
|
|
615
620
|
</Box>
|
|
616
621
|
<Table
|
|
617
|
-
data={
|
|
622
|
+
data={data.list}
|
|
618
623
|
columns={columns}
|
|
624
|
+
onChange={onTableChange}
|
|
619
625
|
options={{
|
|
620
|
-
count:
|
|
621
|
-
page:
|
|
622
|
-
rowsPerPage:
|
|
623
|
-
onChangePage: handlePageChange,
|
|
624
|
-
onChangeRowsPerPage: handleRowsPerPageChange,
|
|
625
|
-
search: false,
|
|
626
|
-
filter: false,
|
|
627
|
-
sort: false,
|
|
628
|
-
viewColumns: false,
|
|
629
|
-
download: false,
|
|
630
|
-
print: false,
|
|
631
|
-
selectableRows: 'none',
|
|
626
|
+
count: data.count,
|
|
627
|
+
page: search!.page - 1,
|
|
628
|
+
rowsPerPage: search!.pageSize,
|
|
632
629
|
responsive: isMobile ? 'vertical' : 'standard',
|
|
633
630
|
}}
|
|
634
|
-
loading={
|
|
631
|
+
loading={loading}
|
|
635
632
|
emptyNodeText={t('admin.meter.events.empty')}
|
|
636
633
|
/>
|
|
637
634
|
{/* 日期选择器弹窗 */}
|