payment-kit 1.21.13 → 1.21.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 (56) hide show
  1. package/api/src/crons/payment-stat.ts +31 -23
  2. package/api/src/libs/invoice.ts +29 -4
  3. package/api/src/libs/product.ts +28 -4
  4. package/api/src/routes/checkout-sessions.ts +46 -1
  5. package/api/src/routes/index.ts +2 -0
  6. package/api/src/routes/invoices.ts +63 -2
  7. package/api/src/routes/payment-stats.ts +244 -22
  8. package/api/src/routes/products.ts +3 -0
  9. package/api/src/routes/tax-rates.ts +220 -0
  10. package/api/src/store/migrations/20251001-add-tax-code-to-products.ts +20 -0
  11. package/api/src/store/migrations/20251001-create-tax-rates.ts +17 -0
  12. package/api/src/store/migrations/20251007-relate-tax-rate-to-invoice.ts +24 -0
  13. package/api/src/store/migrations/20251009-add-tax-behavior.ts +21 -0
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/invoice-item.ts +10 -0
  16. package/api/src/store/models/price.ts +7 -0
  17. package/api/src/store/models/product.ts +7 -0
  18. package/api/src/store/models/tax-rate.ts +352 -0
  19. package/api/tests/models/tax-rate.spec.ts +777 -0
  20. package/blocklet.yml +2 -2
  21. package/package.json +6 -6
  22. package/public/currencies/dollar.png +0 -0
  23. package/src/components/collapse.tsx +3 -2
  24. package/src/components/drawer-form.tsx +2 -1
  25. package/src/components/invoice/list.tsx +38 -3
  26. package/src/components/invoice/table.tsx +48 -2
  27. package/src/components/metadata/form.tsx +2 -2
  28. package/src/components/payment-intent/list.tsx +19 -3
  29. package/src/components/payouts/list.tsx +19 -3
  30. package/src/components/price/currency-select.tsx +105 -48
  31. package/src/components/price/form.tsx +3 -1
  32. package/src/components/product/form.tsx +79 -5
  33. package/src/components/refund/list.tsx +20 -3
  34. package/src/components/subscription/items/actions.tsx +25 -15
  35. package/src/components/subscription/list.tsx +15 -3
  36. package/src/components/tax/actions.tsx +140 -0
  37. package/src/components/tax/filter-toolbar.tsx +230 -0
  38. package/src/components/tax/tax-code-select.tsx +633 -0
  39. package/src/components/tax/tax-rate-form.tsx +177 -0
  40. package/src/components/tax/tax-utils.ts +38 -0
  41. package/src/components/tax/taxCodes.json +10882 -0
  42. package/src/components/uploader.tsx +3 -0
  43. package/src/hooks/cache-state.ts +84 -0
  44. package/src/locales/en.tsx +152 -0
  45. package/src/locales/zh.tsx +149 -0
  46. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  47. package/src/pages/admin/index.tsx +2 -0
  48. package/src/pages/admin/overview.tsx +1114 -322
  49. package/src/pages/admin/products/vendors/index.tsx +4 -2
  50. package/src/pages/admin/tax/create.tsx +104 -0
  51. package/src/pages/admin/tax/detail.tsx +476 -0
  52. package/src/pages/admin/tax/edit.tsx +126 -0
  53. package/src/pages/admin/tax/index.tsx +86 -0
  54. package/src/pages/admin/tax/list.tsx +334 -0
  55. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  56. package/src/pages/home.tsx +6 -3
@@ -2,12 +2,14 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { FormInput, FormLabel, Collapse } from '@blocklet/payment-react';
4
4
  import type { InferFormType, TProduct } from '@blocklet/payment-types';
5
- import { Box, Stack, Typography, Select, MenuItem } from '@mui/material';
5
+ import { Box, Stack, Typography, Select, MenuItem, Link } from '@mui/material';
6
6
  import { useFormContext, useWatch, Controller } from 'react-hook-form';
7
+ import { Link as RouterLink } from 'react-router-dom';
7
8
 
8
9
  import MetadataForm from '../metadata/form';
9
10
  import type { Price } from '../price/form';
10
11
  import Uploader from '../uploader';
12
+ import TaxCodeSelect from '../tax/tax-code-select';
11
13
  import ProductFeatures from './features';
12
14
  import VendorConfig from './vendor-config';
13
15
 
@@ -52,10 +54,56 @@ export default function ProductForm({ simple = false }: Props) {
52
54
  render={({ field }) => (
53
55
  <Box sx={{ width: '100%' }}>
54
56
  <FormLabel sx={{ color: 'text.primary', fontSize: '0.875rem' }}>{t('admin.product.type.label')}</FormLabel>
55
- <Select {...field} fullWidth size="small">
56
- <MenuItem value="good">{t('admin.product.type.good')}</MenuItem>
57
- <MenuItem value="service">{t('admin.product.type.service')}</MenuItem>
58
- <MenuItem value="credit">{t('admin.product.type.credit')}</MenuItem>
57
+ <Select
58
+ {...field}
59
+ fullWidth
60
+ size="small"
61
+ renderValue={(value) => {
62
+ const typeMap: Record<string, string> = {
63
+ good: t('admin.product.type.good'),
64
+ service: t('admin.product.type.service'),
65
+ credit: t('admin.product.type.credit'),
66
+ };
67
+ return typeMap[value as string] || value;
68
+ }}>
69
+ <MenuItem value="good">
70
+ <Box>
71
+ <Typography variant="body2">{t('admin.product.type.good')}</Typography>
72
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
73
+ {t('admin.product.type.goodDesc')}
74
+ </Typography>
75
+ </Box>
76
+ </MenuItem>
77
+ <MenuItem value="service">
78
+ <Box>
79
+ <Typography variant="body2">{t('admin.product.type.service')}</Typography>
80
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
81
+ {t('admin.product.type.serviceDesc')}
82
+ </Typography>
83
+ </Box>
84
+ </MenuItem>
85
+ <MenuItem value="credit">
86
+ <Box>
87
+ <Typography variant="body2">{t('admin.product.type.credit')}</Typography>
88
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
89
+ {t('admin.product.type.creditDesc')}
90
+ {' - '}
91
+ <Link
92
+ component={RouterLink}
93
+ to="/admin/billing/meters"
94
+ target="_blank"
95
+ rel="noopener noreferrer"
96
+ sx={{
97
+ color: 'primary.main',
98
+ fontWeight: 500,
99
+ '&:hover': { textDecoration: 'underline' },
100
+ }}
101
+ onClick={(e) => e.stopPropagation()}>
102
+ {t('admin.product.type.creditDescLink')}
103
+ </Link>
104
+ </Typography>
105
+ </Box>
106
+ </MenuItem>
59
107
  </Select>
60
108
  </Box>
61
109
  )}
@@ -120,6 +168,32 @@ export default function ProductForm({ simple = false }: Props) {
120
168
  </Box>
121
169
  </Stack>
122
170
  <Stack sx={{ '& .vendor-config': { mt: 2 } }}>
171
+ <Box sx={{ mb: 2 }}>
172
+ <FormLabel
173
+ description={
174
+ <>
175
+ {t('admin.taxRate.taxCodeDescription')}
176
+ <Link
177
+ component={RouterLink}
178
+ to="/admin/tax"
179
+ target="_blank"
180
+ rel="noopener noreferrer"
181
+ sx={{
182
+ textDecoration: 'none',
183
+ color: 'primary.main',
184
+ mx: 0.5,
185
+ fontWeight: 500,
186
+ '&:hover': { textDecoration: 'underline' },
187
+ }}>
188
+ {t('admin.taxRate.taxCodeDescriptionLink')}
189
+ </Link>
190
+ {t('admin.taxRate.taxCodeDescriptionSuffix')}
191
+ </>
192
+ }>
193
+ {t('admin.taxRate.taxCode')}
194
+ </FormLabel>
195
+ <TaxCodeSelect label="" />
196
+ </Box>
123
197
  <Collapse trigger={t('admin.product.vendorConfig.title')}>
124
198
  <VendorConfig />
125
199
  </Collapse>
@@ -11,14 +11,14 @@ import {
11
11
  } from '@blocklet/payment-react';
12
12
  import type { TRefundExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
- import { useLocalStorageState } from 'ahooks';
15
14
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
15
+ import { Link, useSearchParams } from 'react-router-dom';
17
16
 
18
17
  import { capitalize, toLower } from 'lodash';
19
18
  import CustomerLink from '../customer/link';
20
19
  import FilterToolbar from '../filter-toolbar';
21
20
  import RefundActions from './actions';
21
+ import { useCacheState } from '../../hooks/cache-state';
22
22
 
23
23
  const fetchData = (params: Record<string, any> = {}): Promise<{ list: TRefundExpanded[]; count: number }> => {
24
24
  const search = new URLSearchParams();
@@ -39,6 +39,7 @@ type SearchProps = {
39
39
  pageSize: number;
40
40
  page: number;
41
41
  customer_id?: string;
42
+ currency_id?: string;
42
43
  invoice_id?: string;
43
44
  subscription_id?: string;
44
45
  payment_intent_id?: string;
@@ -90,9 +91,12 @@ export default function RefundList({
90
91
  payment_intent_id = '',
91
92
  }: ListProps) {
92
93
  const { t } = useLocaleContext();
94
+ const [searchParams] = useSearchParams();
95
+
93
96
  const listKey = getListKey({ customer_id, invoice_id, subscription_id, payment_intent_id });
94
97
  const defaultPageSize = useDefaultPageSize(20);
95
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
98
+
99
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
96
100
  defaultValue: {
97
101
  status: status as string,
98
102
  customer_id,
@@ -102,6 +106,19 @@ export default function RefundList({
102
106
  pageSize: defaultPageSize,
103
107
  page: 1,
104
108
  },
109
+ getUrlParams: () => {
110
+ const params: Record<string, any> = {};
111
+ if (searchParams.has('status')) {
112
+ params.status = searchParams.get('status');
113
+ }
114
+ if (searchParams.has('currency_id')) {
115
+ params.currency_id = searchParams.get('currency_id');
116
+ }
117
+ if (searchParams.has('customer_id')) {
118
+ params.customer_id = searchParams.get('customer_id');
119
+ }
120
+ return params;
121
+ },
105
122
  });
106
123
 
107
124
  const [data, setData] = useState({}) as any;
@@ -7,27 +7,37 @@ import ClickBoundary from '../../click-boundary';
7
7
 
8
8
  type Props = {
9
9
  data: TLineItemExpanded;
10
+ mode?: 'admin' | 'portal';
10
11
  };
11
12
 
12
- export default function LineItemActions(props: Props) {
13
+ export default function LineItemActions({ data, mode = 'portal' }: Props) {
13
14
  const { t } = useLocaleContext();
14
15
  const navigate = useNavigate();
16
+ const isAdmin = mode === 'admin';
17
+ const taxRateId = (data as any).tax_rate_id;
18
+
19
+ const actions = [
20
+ {
21
+ label: t('admin.price.view'),
22
+ handler: () => navigate(`/admin/products/${data.price_id}`),
23
+ color: 'primary',
24
+ },
25
+ {
26
+ label: t('admin.product.view'),
27
+ handler: () => navigate(`/admin/products/${data.product_id || data.price.product_id}`),
28
+ color: 'primary',
29
+ },
30
+ isAdmin &&
31
+ taxRateId && {
32
+ label: t('admin.taxRate.view'),
33
+ handler: () => navigate(`/admin/tax/${taxRateId}`),
34
+ color: 'primary',
35
+ },
36
+ ].filter(Boolean);
37
+
15
38
  return (
16
39
  <ClickBoundary>
17
- <Actions
18
- actions={[
19
- {
20
- label: t('admin.price.view'),
21
- handler: () => navigate(`/admin/products/${props.data.price_id}`),
22
- color: 'primary',
23
- },
24
- {
25
- label: t('admin.product.view'),
26
- handler: () => navigate(`/admin/products/${props.data.product_id || props.data.price.product_id}`),
27
- color: 'primary',
28
- },
29
- ]}
30
- />
40
+ <Actions actions={actions} />
31
41
  </ClickBoundary>
32
42
  );
33
43
  }
@@ -3,10 +3,10 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import { Status, api, formatTime, Table, useDefaultPageSize } from '@blocklet/payment-react';
4
4
  import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
5
  import { CircularProgress } from '@mui/material';
6
- import { useLocalStorageState } from 'ahooks';
7
6
  import { useEffect, useState } from 'react';
8
- import { Link } from 'react-router-dom';
7
+ import { Link, useSearchParams } from 'react-router-dom';
9
8
 
9
+ import { useCacheState } from '../../hooks/cache-state';
10
10
  import CustomerLink from '../customer/link';
11
11
  import FilterToolbar from '../filter-toolbar';
12
12
  import SubscriptionActions from './actions';
@@ -64,11 +64,13 @@ export default function SubscriptionList({
64
64
  },
65
65
  status = '',
66
66
  }: ListProps) {
67
+ const [searchParams] = useSearchParams();
67
68
  const listKey = getListKey({ customer_id });
68
69
 
69
70
  const { t } = useLocaleContext();
70
71
  const defaultPageSize = useDefaultPageSize(20);
71
- const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
72
+
73
+ const [search, setSearch] = useCacheState<SearchProps>(listKey, {
72
74
  defaultValue: {
73
75
  status: (status || 'active') as string,
74
76
  customer_id,
@@ -76,6 +78,16 @@ export default function SubscriptionList({
76
78
  page: 1,
77
79
  price_id: '',
78
80
  },
81
+ getUrlParams: () => {
82
+ const params: Record<string, any> = {};
83
+ if (searchParams.has('status')) {
84
+ params.status = searchParams.get('status');
85
+ }
86
+ if (searchParams.has('customer_id')) {
87
+ params.customer_id = searchParams.get('customer_id');
88
+ }
89
+ return params;
90
+ },
79
91
  });
80
92
 
81
93
  const [data, setData] = useState({}) as any;
@@ -0,0 +1,140 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
4
+ import { useState } from 'react';
5
+ import { useNavigate } from 'react-router-dom';
6
+ import ClickBoundary from '../click-boundary';
7
+ import Actions from '../actions';
8
+ import TaxRateEdit from '../../pages/admin/tax/edit';
9
+
10
+ type TaxRate = {
11
+ id: string;
12
+ display_name: string;
13
+ description?: string;
14
+ country: string;
15
+ state?: string;
16
+ postal_code?: string;
17
+ tax_code?: string;
18
+ percentage: number;
19
+ active: boolean;
20
+ invoice_count?: number;
21
+ metadata?: Record<string, any>;
22
+ created_at?: string;
23
+ updated_at?: string;
24
+ };
25
+
26
+ type Props = {
27
+ data: TaxRate;
28
+ onChange?: () => void;
29
+ };
30
+
31
+ export default function TaxRateActions({ data, onChange = () => {} }: Props) {
32
+ const { t } = useLocaleContext();
33
+ const navigate = useNavigate();
34
+ const [state, setState] = useState<{ action: '' | 'toggle' | 'delete' | 'edit'; loading: boolean }>({
35
+ action: '',
36
+ loading: false,
37
+ });
38
+
39
+ const finish = () => {
40
+ setState({ action: '', loading: false });
41
+ onChange();
42
+ };
43
+
44
+ const handleToggle = async () => {
45
+ try {
46
+ setState({ action: 'toggle', loading: true });
47
+ await api.put(`/api/tax-rates/${data.id}`, { active: !data.active });
48
+ Toast.success(t('admin.taxRate.updated'));
49
+ finish();
50
+ } catch (error) {
51
+ console.error(error);
52
+ Toast.error(formatError(error));
53
+ setState({ action: '', loading: false });
54
+ }
55
+ };
56
+
57
+ const handleDelete = async () => {
58
+ try {
59
+ setState({ action: 'delete', loading: true });
60
+ await api.delete(`/api/tax-rates/${data.id}`);
61
+ Toast.success(t('admin.taxRate.deleted'));
62
+ finish();
63
+ } catch (error) {
64
+ console.error(error);
65
+ Toast.error(formatError(error));
66
+ setState({ action: '', loading: false });
67
+ }
68
+ };
69
+
70
+ const invoiceCount = Number(data.invoice_count) || 0;
71
+ const canDelete = invoiceCount === 0;
72
+
73
+ const actions = [
74
+ {
75
+ label: t('common.view'),
76
+ color: 'primary',
77
+ handler: () => navigate(`/admin/tax/${data.id}`),
78
+ },
79
+ {
80
+ label: t('common.edit'),
81
+ color: 'primary',
82
+ handler: () => setState({ action: 'edit', loading: false }),
83
+ },
84
+ data.active
85
+ ? {
86
+ label: t('common.deactivate'),
87
+ color: 'secondary',
88
+ handler: () => setState({ action: 'toggle', loading: false }),
89
+ }
90
+ : {
91
+ label: t('common.activate'),
92
+ color: 'secondary',
93
+ handler: () => setState({ action: 'toggle', loading: false }),
94
+ },
95
+ canDelete
96
+ ? {
97
+ label: t('common.delete'),
98
+ color: 'error',
99
+ handler: () => setState({ action: 'delete', loading: false }),
100
+ divider: true,
101
+ }
102
+ : null,
103
+ ].filter(Boolean) as any[];
104
+
105
+ return (
106
+ <ClickBoundary>
107
+ <Actions actions={actions} variant="compact" />
108
+ {state.action === 'edit' && (
109
+ <TaxRateEdit
110
+ id={data.id}
111
+ initialData={data}
112
+ onClose={() => setState({ action: '', loading: false })}
113
+ onChange={onChange}
114
+ />
115
+ )}
116
+ {state.action === 'toggle' && (
117
+ <ConfirmDialog
118
+ onConfirm={handleToggle}
119
+ onCancel={() => setState({ action: '', loading: false })}
120
+ loading={state.loading}
121
+ title={data.active ? t('admin.taxRate.deactivateTitle') : t('admin.taxRate.activateTitle')}
122
+ message={
123
+ data.active
124
+ ? t('admin.taxRate.deactivateMessage', { name: data.display_name })
125
+ : t('admin.taxRate.activateMessage', { name: data.display_name })
126
+ }
127
+ />
128
+ )}
129
+ {state.action === 'delete' && (
130
+ <ConfirmDialog
131
+ onConfirm={handleDelete}
132
+ onCancel={() => setState({ action: '', loading: false })}
133
+ loading={state.loading}
134
+ title={t('admin.taxRate.deleteTitle')}
135
+ message={t('admin.taxRate.deleteMessage', { name: data.display_name })}
136
+ />
137
+ )}
138
+ </ClickBoundary>
139
+ );
140
+ }
@@ -0,0 +1,230 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Add, Close } from '@mui/icons-material';
3
+ import { Box, Button, ListItemIcon, ListItemText, Menu, MenuItem, TextField, styled } from '@mui/material';
4
+ import { defaultCountries, parseCountry, FlagEmoji } from 'react-international-phone';
5
+ import type { CountryIso2 } from 'react-international-phone';
6
+ import { useMemo, useState } from 'react';
7
+
8
+ type SearchState = {
9
+ active?: string;
10
+ country?: string;
11
+ q?: string;
12
+ };
13
+
14
+ type Props = {
15
+ search?: SearchState | null;
16
+ onFilterChange: (key: keyof SearchState, value: string) => void;
17
+ };
18
+
19
+ const Root = styled(Box)`
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 12px;
23
+
24
+ @media (min-width: 960px) {
25
+ flex-direction: row;
26
+ align-items: center;
27
+ justify-content: space-between;
28
+ }
29
+
30
+ .filters {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ gap: 8px;
35
+ }
36
+
37
+ .filter-trigger {
38
+ display: inline-flex;
39
+ align-items: center;
40
+ border-radius: 24px;
41
+ background-color: ${({ theme }) => theme.palette.grey[100]};
42
+ color: ${({ theme }) => theme.palette.text.secondary};
43
+ padding: 6px 12px;
44
+ font-size: 14px;
45
+ line-height: 20px;
46
+ text-transform: none;
47
+ }
48
+
49
+ .filter-trigger span {
50
+ color: ${({ theme }) => theme.palette.primary.main};
51
+ padding-left: 4px;
52
+ }
53
+ `;
54
+
55
+ export default function TaxFilterToolbar({ search = {}, onFilterChange }: Props) {
56
+ const { t } = useLocaleContext();
57
+ const [statusAnchor, setStatusAnchor] = useState<null | HTMLElement>(null);
58
+ const [countryAnchor, setCountryAnchor] = useState<null | HTMLElement>(null);
59
+ const [countrySearch, setCountrySearch] = useState('');
60
+
61
+ const statusLabel = useMemo(() => {
62
+ if (!search?.active) return '';
63
+ if (search.active === 'active') return t('common.active');
64
+ if (search.active === 'inactive') return t('common.inactive');
65
+ return '';
66
+ }, [search?.active, t]);
67
+
68
+ type ParsedCountry = ReturnType<typeof parseCountry>;
69
+
70
+ const countries = useMemo<ParsedCountry[]>(() => {
71
+ const term = countrySearch.trim().toLowerCase();
72
+ if (!term) {
73
+ return defaultCountries.map((country) => parseCountry(country));
74
+ }
75
+ return defaultCountries
76
+ .map((country) => parseCountry(country))
77
+ .filter(
78
+ (country) =>
79
+ country.name.toLowerCase().includes(term) ||
80
+ country.iso2.toLowerCase().includes(term) ||
81
+ `+${country.dialCode}`.includes(term)
82
+ );
83
+ }, [countrySearch]);
84
+
85
+ const selectedCountry = useMemo<ParsedCountry | { iso2: CountryIso2; name: string; dialCode: string } | null>(() => {
86
+ if (!search?.country) return null;
87
+ const match = defaultCountries.find((item) => item[1] === search.country);
88
+ if (!match) {
89
+ return {
90
+ iso2: search.country.toLowerCase() as CountryIso2,
91
+ name: search.country.toUpperCase(),
92
+ dialCode: '',
93
+ };
94
+ }
95
+ return parseCountry(match);
96
+ }, [search?.country]);
97
+
98
+ const closeCountryMenu = () => {
99
+ setCountrySearch('');
100
+ setCountryAnchor(null);
101
+ };
102
+
103
+ return (
104
+ <Root>
105
+ <Box className="filters">
106
+ <Button
107
+ className="filter-trigger"
108
+ variant="text"
109
+ onClick={(event) => setStatusAnchor(event.currentTarget as HTMLElement)}>
110
+ {search?.active ? (
111
+ <Close
112
+ sx={{ fontSize: 18, mr: 1 }}
113
+ onClick={(event) => {
114
+ event.stopPropagation();
115
+ onFilterChange('active', '');
116
+ }}
117
+ />
118
+ ) : (
119
+ <Add sx={{ fontSize: 18, mr: 1 }} />
120
+ )}
121
+ {t('common.status')}
122
+ {statusLabel && <span>{statusLabel}</span>}
123
+ </Button>
124
+ <Menu
125
+ anchorEl={statusAnchor}
126
+ open={Boolean(statusAnchor)}
127
+ onClose={() => setStatusAnchor(null)}
128
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
129
+ transformOrigin={{ vertical: 'top', horizontal: 'left' }}>
130
+ <MenuItem
131
+ onClick={() => {
132
+ onFilterChange('active', 'active');
133
+ setStatusAnchor(null);
134
+ }}>
135
+ {t('common.active')}
136
+ </MenuItem>
137
+ <MenuItem
138
+ onClick={() => {
139
+ onFilterChange('active', 'inactive');
140
+ setStatusAnchor(null);
141
+ }}>
142
+ {t('common.inactive')}
143
+ </MenuItem>
144
+ </Menu>
145
+
146
+ <Button
147
+ className="filter-trigger"
148
+ variant="text"
149
+ onClick={(event) => {
150
+ setCountrySearch('');
151
+ setCountryAnchor(event.currentTarget as HTMLElement);
152
+ }}>
153
+ {search?.country ? (
154
+ <Close
155
+ sx={{ fontSize: 18, mr: 1 }}
156
+ onClick={(event) => {
157
+ event.stopPropagation();
158
+ onFilterChange('country', '');
159
+ }}
160
+ />
161
+ ) : (
162
+ <Add sx={{ fontSize: 18, mr: 1 }} />
163
+ )}
164
+ {t('admin.taxRate.country')}
165
+ {selectedCountry && (
166
+ <Box
167
+ sx={{
168
+ display: 'inline-flex',
169
+ alignItems: 'center',
170
+ gap: 0.75,
171
+ ml: 0.5,
172
+ color: 'primary.main',
173
+ }}>
174
+ <FlagEmoji iso2={selectedCountry.iso2 as CountryIso2} style={{ fontSize: 16 }} />
175
+ {selectedCountry.name}
176
+ </Box>
177
+ )}
178
+ </Button>
179
+ <Menu
180
+ anchorEl={countryAnchor}
181
+ open={Boolean(countryAnchor)}
182
+ onClose={() => closeCountryMenu()}
183
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
184
+ transformOrigin={{ vertical: 'top', horizontal: 'left' }}>
185
+ <Box
186
+ sx={{
187
+ p: 2,
188
+ display: 'flex',
189
+ flexDirection: 'column',
190
+ gap: 1.5,
191
+ }}>
192
+ <TextField
193
+ size="small"
194
+ placeholder={t('admin.taxRate.searchCountry')}
195
+ value={countrySearch}
196
+ onChange={(event) => setCountrySearch(event.target.value)}
197
+ autoFocus
198
+ />
199
+ <Box
200
+ sx={{
201
+ maxHeight: 260,
202
+ overflowY: 'auto',
203
+ border: '1px solid',
204
+ borderColor: 'divider',
205
+ borderRadius: 1,
206
+ }}>
207
+ {countries.map((country) => (
208
+ <MenuItem
209
+ key={country.iso2}
210
+ selected={country.iso2 === search?.country}
211
+ onClick={() => {
212
+ onFilterChange('country', country.iso2);
213
+ closeCountryMenu();
214
+ }}>
215
+ <ListItemIcon sx={{ minWidth: 32, fontSize: 18, color: 'text.secondary' }}>
216
+ <FlagEmoji iso2={country.iso2 as CountryIso2} />
217
+ </ListItemIcon>
218
+ <ListItemText primary={country.name} />
219
+ </MenuItem>
220
+ ))}
221
+ {!countries.length && (
222
+ <Box sx={{ py: 1.5, px: 2, fontSize: 14, color: 'text.secondary' }}>{t('common.noData')}</Box>
223
+ )}
224
+ </Box>
225
+ </Box>
226
+ </Menu>
227
+ </Box>
228
+ </Root>
229
+ );
230
+ }