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.
Files changed (37) hide show
  1. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  2. package/api/src/libs/vendor-util/adapters/types.ts +2 -3
  3. package/api/src/libs/vendor-util/fulfillment.ts +16 -30
  4. package/api/src/queues/vendors/commission.ts +32 -42
  5. package/api/src/queues/vendors/fulfillment-coordinator.ts +68 -60
  6. package/api/src/queues/vendors/fulfillment.ts +5 -5
  7. package/api/src/queues/vendors/return-processor.ts +0 -1
  8. package/api/src/queues/vendors/status-check.ts +2 -2
  9. package/api/src/routes/checkout-sessions.ts +15 -2
  10. package/api/src/routes/coupons.ts +7 -0
  11. package/api/src/routes/credit-grants.ts +8 -1
  12. package/api/src/routes/credit-transactions.ts +153 -13
  13. package/api/src/routes/invoices.ts +35 -1
  14. package/api/src/routes/meter-events.ts +31 -3
  15. package/api/src/routes/meters.ts +4 -0
  16. package/api/src/routes/payment-currencies.ts +2 -1
  17. package/api/src/routes/promotion-codes.ts +2 -2
  18. package/api/src/routes/subscription-items.ts +4 -0
  19. package/api/src/routes/vendor.ts +13 -4
  20. package/api/src/routes/webhook-endpoints.ts +4 -0
  21. package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
  22. package/api/src/store/models/checkout-session.ts +23 -0
  23. package/api/src/store/models/credit-transaction.ts +5 -0
  24. package/api/src/store/models/meter-event.ts +22 -12
  25. package/api/src/store/models/types.ts +18 -0
  26. package/blocklet.yml +1 -1
  27. package/package.json +5 -5
  28. package/src/components/customer/credit-overview.tsx +1 -1
  29. package/src/components/customer/related-credit-grants.tsx +194 -0
  30. package/src/components/meter/add-usage-dialog.tsx +8 -0
  31. package/src/components/meter/events-list.tsx +93 -96
  32. package/src/components/product/form.tsx +0 -1
  33. package/src/locales/en.tsx +9 -0
  34. package/src/locales/zh.tsx +9 -0
  35. package/src/pages/admin/billing/invoices/detail.tsx +21 -2
  36. package/src/pages/customer/invoice/detail.tsx +11 -2
  37. 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(this.GENESIS_ATTRIBUTES, {
219
- sequelize,
220
- modelName: 'MeterEvent',
221
- tableName: 'meter_events',
222
- createdAt: 'created_at',
223
- updatedAt: 'updated_at',
224
- indexes: [{ fields: ['identifier'], unique: true }, { fields: ['status'] }, { fields: ['event_name'] }],
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
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.20.13
17
+ version: 1.20.15
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.20.13",
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.13",
60
- "@blocklet/payment-vendor": "1.20.13",
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.13",
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": "0cbe918549f7d06561b5307fb947bfbbd2250984"
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 = (meterId: string, params: any): Promise<{ list: TMeterEventExpanded[]; count: number }> => {
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
- Object.keys(params).forEach((key) => {
73
- if (params[key] !== undefined && params[key] !== '') {
74
- searchParams.append(key, params[key]);
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 handlePageChange = (page: number) => {
279
- loadEvents(page, state.pagination.limit);
280
- };
281
-
282
- const handleRowsPerPageChange = (rowsPerPage: number) => {
283
- loadEvents(0, rowsPerPage);
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
- loadEvents(0, state.pagination.limit);
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 = state.events[index];
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 = state.events[index];
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 = state.events[index];
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 = state.events[index];
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: state.pagination.total })}
618
+ {t('admin.meter.events.count', { count: data.count })}
614
619
  </Typography>
615
620
  </Box>
616
621
  <Table
617
- data={state.events}
622
+ data={data.list}
618
623
  columns={columns}
624
+ onChange={onTableChange}
619
625
  options={{
620
- count: state.pagination.total,
621
- page: state.pagination.page,
622
- rowsPerPage: state.pagination.limit,
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={state.loading}
631
+ loading={loading}
635
632
  emptyNodeText={t('admin.meter.events.empty')}
636
633
  />
637
634
  {/* 日期选择器弹窗 */}
@@ -109,7 +109,6 @@ export default function ProductForm({ simple = false }: Props) {
109
109
  required
110
110
  minRows={2}
111
111
  maxRows={4}
112
- inputProps={{ maxLength: 250 }}
113
112
  />
114
113
  </Stack>
115
114