payment-kit 1.21.13 → 1.21.14

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 (55) 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 -1
  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 -1
  29. package/src/components/payouts/list.tsx +19 -1
  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 -1
  34. package/src/components/subscription/items/actions.tsx +25 -15
  35. package/src/components/subscription/list.tsx +16 -1
  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/locales/en.tsx +152 -0
  44. package/src/locales/zh.tsx +149 -0
  45. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  46. package/src/pages/admin/index.tsx +2 -0
  47. package/src/pages/admin/overview.tsx +1114 -322
  48. package/src/pages/admin/products/vendors/index.tsx +4 -2
  49. package/src/pages/admin/tax/create.tsx +104 -0
  50. package/src/pages/admin/tax/detail.tsx +476 -0
  51. package/src/pages/admin/tax/edit.tsx +126 -0
  52. package/src/pages/admin/tax/index.tsx +86 -0
  53. package/src/pages/admin/tax/list.tsx +334 -0
  54. package/src/pages/customer/subscription/change-payment.tsx +1 -1
  55. 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>
@@ -13,7 +13,7 @@ import type { TRefundExpanded } from '@blocklet/payment-types';
13
13
  import { Avatar, CircularProgress, Typography } from '@mui/material';
14
14
  import { useLocalStorageState } from 'ahooks';
15
15
  import { useEffect, useState } from 'react';
16
- import { Link } from 'react-router-dom';
16
+ import { Link, useSearchParams } from 'react-router-dom';
17
17
 
18
18
  import { capitalize, toLower } from 'lodash';
19
19
  import CustomerLink from '../customer/link';
@@ -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,8 +91,15 @@ 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);
98
+
99
+ const urlStatus = searchParams.get('status');
100
+ const urlCurrencyId = searchParams.get('currency_id');
101
+ const urlCustomerId = searchParams.get('customer_id');
102
+
95
103
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
96
104
  defaultValue: {
97
105
  status: status as string,
@@ -104,6 +112,17 @@ export default function RefundList({
104
112
  },
105
113
  });
106
114
 
115
+ useEffect(() => {
116
+ if (urlStatus || urlCurrencyId || urlCustomerId) {
117
+ setSearch((prev) => ({
118
+ ...prev!,
119
+ ...(urlStatus && { status: urlStatus }),
120
+ ...(urlCurrencyId && { currency_id: urlCurrencyId }),
121
+ ...(urlCustomerId && { customer_id: urlCustomerId }),
122
+ }));
123
+ }
124
+ }, [urlStatus, urlCurrencyId, urlCustomerId, setSearch]);
125
+
107
126
  const [data, setData] = useState({}) as any;
108
127
 
109
128
  const refresh = () =>
@@ -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
  }
@@ -5,7 +5,7 @@ import type { TSubscriptionExpanded } from '@blocklet/payment-types';
5
5
  import { CircularProgress } from '@mui/material';
6
6
  import { useLocalStorageState } from 'ahooks';
7
7
  import { useEffect, useState } from 'react';
8
- import { Link } from 'react-router-dom';
8
+ import { Link, useSearchParams } from 'react-router-dom';
9
9
 
10
10
  import CustomerLink from '../customer/link';
11
11
  import FilterToolbar from '../filter-toolbar';
@@ -64,10 +64,15 @@ 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);
72
+
73
+ const urlStatus = searchParams.get('status');
74
+ const urlCustomerId = searchParams.get('customer_id');
75
+
71
76
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
72
77
  defaultValue: {
73
78
  status: (status || 'active') as string,
@@ -78,6 +83,16 @@ export default function SubscriptionList({
78
83
  },
79
84
  });
80
85
 
86
+ useEffect(() => {
87
+ if (urlStatus || urlCustomerId) {
88
+ setSearch((prev) => ({
89
+ ...prev!,
90
+ ...(urlStatus && { status: urlStatus }),
91
+ ...(urlCustomerId && { customer_id: urlCustomerId }),
92
+ }));
93
+ }
94
+ }, [urlStatus, urlCustomerId, setSearch]);
95
+
81
96
  const [data, setData] = useState({}) as any;
82
97
 
83
98
  const refresh = () =>
@@ -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
+ }