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
@@ -0,0 +1,633 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import {
3
+ Box,
4
+ Button,
5
+ Divider,
6
+ Drawer,
7
+ IconButton,
8
+ InputAdornment,
9
+ List,
10
+ ListItem,
11
+ ListItemButton,
12
+ ListItemIcon,
13
+ ListItemText,
14
+ Popover,
15
+ Stack,
16
+ TextField,
17
+ Typography,
18
+ } from '@mui/material';
19
+ import { CheckCircle, Close, KeyboardDoubleArrowRight, Search } from '@mui/icons-material';
20
+ import { useEffect, useMemo, useRef, useState, memo } from 'react';
21
+ import type { ControllerRenderProps, FieldError } from 'react-hook-form';
22
+ import { Controller, useFormContext } from 'react-hook-form';
23
+ import type { MouseEvent } from 'react';
24
+ import { useMobile } from '@blocklet/payment-react';
25
+
26
+ import Collapse from '../collapse';
27
+ import taxCodesRaw from './taxCodes.json';
28
+
29
+ export type TaxCodeType = 'general' | 'digital' | 'services' | 'physical';
30
+
31
+ type LocaleKey = 'en' | 'zh';
32
+
33
+ interface RawTaxCode {
34
+ id: string;
35
+ type: TaxCodeType;
36
+ name: Record<LocaleKey, string>;
37
+ description: Record<LocaleKey, string>;
38
+ label: Record<LocaleKey, string>;
39
+ }
40
+
41
+ const TAX_CODE_TYPES: TaxCodeType[] = ['general', 'digital', 'services', 'physical'];
42
+
43
+ const TYPE_TRANSLATION_KEYS: Record<TaxCodeType, string> = {
44
+ general: 'admin.taxRate.types.general',
45
+ digital: 'admin.taxRate.types.digital',
46
+ services: 'admin.taxRate.types.services',
47
+ physical: 'admin.taxRate.types.physical',
48
+ };
49
+
50
+ const taxCodesById: Record<string, RawTaxCode> = taxCodesRaw as unknown as Record<string, RawTaxCode>;
51
+ const TAX_CODE_LIST: RawTaxCode[] = Object.values(taxCodesById);
52
+
53
+ const SORTED_TAX_CODES: RawTaxCode[] = [...TAX_CODE_LIST].sort((a, b) =>
54
+ a.name.en.localeCompare(b.name.en, undefined, { sensitivity: 'base' })
55
+ );
56
+
57
+ const GROUPED_TAX_CODES: Record<TaxCodeType, RawTaxCode[]> = SORTED_TAX_CODES.reduce(
58
+ (acc, code) => {
59
+ if (!acc[code.type]) {
60
+ acc[code.type] = [] as RawTaxCode[];
61
+ }
62
+ acc[code.type].push(code);
63
+ return acc;
64
+ },
65
+ {
66
+ general: [] as RawTaxCode[],
67
+ digital: [] as RawTaxCode[],
68
+ services: [] as RawTaxCode[],
69
+ physical: [] as RawTaxCode[],
70
+ }
71
+ );
72
+
73
+ const createDefaultExpanded = (): Record<TaxCodeType, boolean> => {
74
+ const result = {} as Record<TaxCodeType, boolean>;
75
+ TAX_CODE_TYPES.forEach((type, index) => {
76
+ result[type] = index === 0;
77
+ });
78
+ return result;
79
+ };
80
+
81
+ interface TaxCodeSelectProps {
82
+ name?: string;
83
+ label?: string;
84
+ placeholder?: string;
85
+ required?: boolean;
86
+ disabled?: boolean;
87
+ }
88
+
89
+ interface PickerProps {
90
+ field: ControllerRenderProps<any, string>;
91
+ error?: FieldError;
92
+ disabled: boolean;
93
+ label: string;
94
+ placeholder: string;
95
+ language: LocaleKey;
96
+ translate: (key: string, params?: Record<string, any>) => string;
97
+ }
98
+
99
+ interface TaxCodeItemProps {
100
+ code: RawTaxCode;
101
+ isSelected: boolean;
102
+ displayName: string;
103
+ showIndicator: boolean;
104
+ itemBorder: boolean;
105
+ onSelect: (code: RawTaxCode) => void;
106
+ onHover: (id: string | null) => void;
107
+ }
108
+
109
+ const TaxCodeItem = memo(
110
+ ({ code, isSelected, displayName, showIndicator, itemBorder, onSelect, onHover }: TaxCodeItemProps) => (
111
+ <ListItem
112
+ disablePadding
113
+ sx={
114
+ itemBorder
115
+ ? {
116
+ '&:not(:last-of-type)': {
117
+ borderBottom: '1px solid',
118
+ borderColor: 'divider',
119
+ },
120
+ contain: 'layout style paint',
121
+ }
122
+ : { contain: 'layout style paint' }
123
+ }>
124
+ <ListItemButton
125
+ selected={isSelected}
126
+ onMouseEnter={() => onHover(code.id)}
127
+ onMouseLeave={() => onHover(null)}
128
+ onFocus={() => onHover(code.id)}
129
+ onClick={() => onSelect(code)}>
130
+ <ListItemText
131
+ primary={
132
+ <Typography variant="body2" sx={{ fontWeight: 500, color: 'text.primary' }}>
133
+ {displayName}
134
+ </Typography>
135
+ }
136
+ secondary={
137
+ <Typography
138
+ variant="caption"
139
+ sx={{
140
+ color: 'text.secondary',
141
+ }}>
142
+ {code.id}
143
+ </Typography>
144
+ }
145
+ />
146
+ {showIndicator && isSelected && (
147
+ <ListItemIcon sx={{ minWidth: 0, color: 'primary.main' }}>
148
+ <CheckCircle fontSize="small" />
149
+ </ListItemIcon>
150
+ )}
151
+ </ListItemButton>
152
+ </ListItem>
153
+ )
154
+ );
155
+
156
+ TaxCodeItem.displayName = 'TaxCodeItem';
157
+
158
+ function TaxCodePicker({ field, error = undefined, disabled, label, placeholder, language, translate }: PickerProps) {
159
+ const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
160
+ const [drawerOpen, setDrawerOpen] = useState(false);
161
+ const [search, setSearch] = useState('');
162
+ const [hoveredId, setHoveredId] = useState<string | null>(null);
163
+ const [expandedSections, setExpandedSections] = useState<Record<TaxCodeType, boolean>>(createDefaultExpanded());
164
+ const [pendingId, setPendingId] = useState<string>(field.value || '');
165
+ const searchInputRef = useRef<HTMLInputElement>(null);
166
+ const { isMobile } = useMobile();
167
+ const sectionRefs = useRef<Record<TaxCodeType, HTMLDivElement | null>>({
168
+ general: null,
169
+ digital: null,
170
+ services: null,
171
+ physical: null,
172
+ });
173
+
174
+ const selected = field.value ? taxCodesById[field.value] : null;
175
+ const previewId = hoveredId || pendingId || field.value || '';
176
+ const preview = previewId ? taxCodesById[previewId] : undefined;
177
+
178
+ const filteredByType = useMemo(() => {
179
+ const term = search.trim().toLowerCase();
180
+ if (!term) {
181
+ return GROUPED_TAX_CODES;
182
+ }
183
+
184
+ const matches: Record<TaxCodeType, RawTaxCode[]> = {
185
+ general: [],
186
+ digital: [],
187
+ services: [],
188
+ physical: [],
189
+ };
190
+
191
+ SORTED_TAX_CODES.forEach((code) => {
192
+ const match =
193
+ code.id.toLowerCase().includes(term) ||
194
+ code.name.en.toLowerCase().includes(term) ||
195
+ code.name.zh.toLowerCase().includes(term) ||
196
+ code.description.en.toLowerCase().includes(term) ||
197
+ code.description.zh.toLowerCase().includes(term);
198
+
199
+ if (match) {
200
+ matches[code.type].push(code);
201
+ }
202
+ });
203
+
204
+ return matches;
205
+ }, [search]);
206
+
207
+ const handleClose = () => {
208
+ setAnchorEl(null);
209
+ setDrawerOpen(false);
210
+ setSearch('');
211
+ setHoveredId(null);
212
+ setExpandedSections(createDefaultExpanded());
213
+ setPendingId(field.value || '');
214
+ };
215
+
216
+ const handleClear = (event: React.MouseEvent) => {
217
+ event.stopPropagation();
218
+ field.onChange('');
219
+ setPendingId('');
220
+ handleClose();
221
+ };
222
+
223
+ const handleSelect = (code: RawTaxCode) => {
224
+ setPendingId(code.id);
225
+ setHoveredId(null);
226
+ };
227
+
228
+ const handleConfirm = () => {
229
+ if (!pendingId) return;
230
+ field.onChange(pendingId);
231
+ handleClose();
232
+ };
233
+
234
+ const displayValue = selected ? `${selected.name[language] || selected.name.en} (${selected.id})` : '';
235
+
236
+ const hasResults = TAX_CODE_TYPES.some((type) => filteredByType[type].length > 0);
237
+ const popoverOpen = Boolean(anchorEl);
238
+
239
+ const toggleSection = (type: TaxCodeType) => {
240
+ setExpandedSections((prev) => {
241
+ if (prev[type]) {
242
+ return { ...prev, [type]: false };
243
+ }
244
+ const next = createDefaultExpanded();
245
+ Object.keys(next).forEach((key) => {
246
+ next[key as TaxCodeType] = false;
247
+ });
248
+ next[type] = true;
249
+ return next;
250
+ });
251
+ requestAnimationFrame(() => {
252
+ const ref = sectionRefs.current[type];
253
+ if (ref) {
254
+ ref.scrollIntoView({ block: 'start', behavior: 'smooth' });
255
+ }
256
+ });
257
+ };
258
+
259
+ const onInputClick = (event: MouseEvent<HTMLElement>) => {
260
+ if (disabled) return;
261
+ if (isMobile) {
262
+ setDrawerOpen(true);
263
+ } else {
264
+ setAnchorEl(event.currentTarget);
265
+ }
266
+ };
267
+
268
+ const onPopoverClose = () => {
269
+ handleClose();
270
+ };
271
+
272
+ useEffect(() => {
273
+ if ((popoverOpen || drawerOpen) && searchInputRef.current) {
274
+ searchInputRef.current.focus();
275
+ }
276
+ if (popoverOpen || drawerOpen) {
277
+ setPendingId(field.value || '');
278
+ setHoveredId(null);
279
+ }
280
+ }, [popoverOpen, drawerOpen, field.value]);
281
+
282
+ useEffect(() => {
283
+ if (search.trim()) {
284
+ setExpandedSections({
285
+ general: true,
286
+ digital: true,
287
+ services: true,
288
+ physical: true,
289
+ });
290
+ } else {
291
+ setExpandedSections(createDefaultExpanded());
292
+ }
293
+ }, [search]);
294
+
295
+ const renderCategories = (options: {
296
+ listPadding?: string | number;
297
+ headerPadding?: string | number;
298
+ itemBorder?: boolean;
299
+ listProps?: Record<string, any>;
300
+ }) =>
301
+ TAX_CODE_TYPES.filter((type) => filteredByType[type].length > 0).map((type) => {
302
+ const isExpanded = expandedSections[type];
303
+ return (
304
+ <Box
305
+ key={type}
306
+ ref={(el) => {
307
+ sectionRefs.current[type] = el as HTMLDivElement;
308
+ }}>
309
+ <Collapse
310
+ trigger={
311
+ <Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
312
+ {translate(TYPE_TRANSLATION_KEYS[type])}
313
+ </Typography>
314
+ }
315
+ expanded={isExpanded}
316
+ onChange={() => toggleSection(type)}
317
+ lazy
318
+ timeout={200}
319
+ style={{
320
+ px: options.headerPadding ?? 1.5,
321
+ py: 1,
322
+ borderBottom: '1px solid',
323
+ borderColor: 'divider',
324
+ backgroundColor: 'background.paper',
325
+ }}>
326
+ <List dense disablePadding {...(options.listProps || {})} sx={{ contain: 'layout style' }}>
327
+ {filteredByType[type].map((code) => {
328
+ const isSelected = pendingId === code.id;
329
+ const displayName = code.name[language] || code.name.en;
330
+ const showIndicator = isSelected || hoveredId === code.id;
331
+ return (
332
+ <TaxCodeItem
333
+ key={code.id}
334
+ code={code}
335
+ isSelected={isSelected}
336
+ displayName={displayName}
337
+ showIndicator={showIndicator}
338
+ itemBorder={options.itemBorder ?? false}
339
+ onSelect={handleSelect}
340
+ onHover={setHoveredId}
341
+ />
342
+ );
343
+ })}
344
+ </List>
345
+ </Collapse>
346
+ </Box>
347
+ );
348
+ });
349
+
350
+ const listContentDesktop = (
351
+ <Box
352
+ sx={{
353
+ flex: 1,
354
+ overflowY: 'auto',
355
+ border: '1px solid',
356
+ borderColor: 'divider',
357
+ borderRadius: 1,
358
+ bgcolor: 'background.paper',
359
+ willChange: 'scroll-position',
360
+ contain: 'layout style paint',
361
+ }}>
362
+ {hasResults ? (
363
+ renderCategories({ headerPadding: 1.5, itemBorder: true })
364
+ ) : (
365
+ <Typography
366
+ variant="body2"
367
+ sx={{
368
+ color: 'text.secondary',
369
+ px: 2,
370
+ py: 4,
371
+ }}>
372
+ {translate('admin.taxRate.noSuggestions')}
373
+ </Typography>
374
+ )}
375
+ </Box>
376
+ );
377
+
378
+ const listContentMobile = (
379
+ <Box
380
+ sx={{
381
+ flex: 1,
382
+ overflowY: 'auto',
383
+ px: 2,
384
+ pb: 2,
385
+ willChange: 'scroll-position',
386
+ contain: 'layout style paint',
387
+ }}>
388
+ {hasResults ? (
389
+ renderCategories({ headerPadding: 1.5, itemBorder: true })
390
+ ) : (
391
+ <Typography
392
+ variant="body2"
393
+ sx={{
394
+ color: 'text.secondary',
395
+ px: 2,
396
+ py: 4,
397
+ }}>
398
+ {translate('admin.taxRate.noSuggestions')}
399
+ </Typography>
400
+ )}
401
+ </Box>
402
+ );
403
+
404
+ const headerContent = (
405
+ <>
406
+ <TextField
407
+ fullWidth
408
+ inputRef={searchInputRef}
409
+ value={search}
410
+ onChange={(event) => setSearch(event.target.value)}
411
+ placeholder={translate('admin.taxRate.searchPlaceholder')}
412
+ slotProps={{
413
+ input: {
414
+ startAdornment: (
415
+ <InputAdornment position="start">
416
+ <Search fontSize="small" />
417
+ </InputAdornment>
418
+ ),
419
+ },
420
+ }}
421
+ size="small"
422
+ />
423
+ {/* <Box
424
+ sx={{
425
+ border: '1px solid',
426
+ borderColor: 'divider',
427
+ borderRadius: 1,
428
+ p: 2,
429
+ bgcolor: 'background.default',
430
+ }}>
431
+ <Typography variant="overline" color="text.secondary">
432
+ {translate('admin.taxRate.usePreset')}
433
+ </Typography>
434
+ <Typography variant="body2" sx={{ mt: 0.5 }}>
435
+ {selected ? selected.name[language] || selected.name.en : translate('admin.taxRate.noPreset')}
436
+ </Typography>
437
+ </Box> */}
438
+ </>
439
+ );
440
+
441
+ return (
442
+ <>
443
+ <TextField
444
+ fullWidth
445
+ value={displayValue}
446
+ onClick={onInputClick}
447
+ label={label}
448
+ placeholder={placeholder}
449
+ error={!!error}
450
+ helperText={error?.message}
451
+ disabled={disabled}
452
+ slotProps={{
453
+ input: {
454
+ endAdornment: (
455
+ <InputAdornment position="end">
456
+ {field.value && !disabled && (
457
+ <IconButton size="small" onClick={handleClear} aria-label={translate('common.clear')}>
458
+ <Close fontSize="small" />
459
+ </IconButton>
460
+ )}
461
+ <KeyboardDoubleArrowRight fontSize="small" color="action" />
462
+ </InputAdornment>
463
+ ),
464
+ },
465
+ htmlInput: {
466
+ readOnly: true,
467
+ },
468
+ }}
469
+ />
470
+ {!isMobile && (
471
+ <Popover
472
+ open={popoverOpen}
473
+ anchorEl={anchorEl}
474
+ onClose={onPopoverClose}
475
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
476
+ transformOrigin={{ vertical: 'top', horizontal: 'left' }}
477
+ slotProps={{
478
+ paper: {
479
+ sx: {
480
+ width: { xs: '95vw', sm: 560, md: 720 },
481
+ maxHeight: 560,
482
+ display: 'flex',
483
+ flexDirection: 'column',
484
+ borderRadius: 2,
485
+ },
486
+ },
487
+ }}>
488
+ <Stack
489
+ direction={{ xs: 'column', sm: 'row' }}
490
+ spacing={0}
491
+ sx={{ flex: 1, minHeight: { xs: 320, sm: 380 }, p: 3, pt: 2 }}>
492
+ <Box sx={{ flex: 1.2, display: 'flex', flexDirection: 'column', gap: 2, pr: { sm: 3 } }}>
493
+ <Box>
494
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
495
+ {translate('admin.taxRate.taxCode')}
496
+ </Typography>
497
+ <Typography
498
+ variant="body2"
499
+ sx={{
500
+ color: 'text.secondary',
501
+ }}>
502
+ {translate('admin.taxRate.selectTaxCodeHint')}
503
+ </Typography>
504
+ </Box>
505
+ {headerContent}
506
+ <Divider />
507
+ {listContentDesktop}
508
+ </Box>
509
+
510
+ <Divider orientation="vertical" flexItem sx={{ display: { xs: 'none', sm: 'block' } }} />
511
+
512
+ <Box
513
+ sx={{
514
+ flex: 1,
515
+ minWidth: 260,
516
+ pl: { sm: 3 },
517
+ pt: { xs: 3, sm: 0 },
518
+ display: 'flex',
519
+ flexDirection: 'column',
520
+ gap: 2,
521
+ }}>
522
+ {preview ? (
523
+ <>
524
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
525
+ {preview.name[language] || preview.name.en}
526
+ </Typography>
527
+ <Typography
528
+ variant="body2"
529
+ sx={{
530
+ color: 'text.secondary',
531
+ }}>
532
+ {preview.description[language] || preview.description.en}
533
+ </Typography>
534
+ <Typography
535
+ variant="caption"
536
+ sx={{
537
+ color: 'text.secondary',
538
+ }}>
539
+ {translate('admin.taxRate.taxCodeLabel', { code: preview.id })}
540
+ </Typography>
541
+ </>
542
+ ) : (
543
+ <Typography
544
+ variant="body2"
545
+ sx={{
546
+ color: 'text.secondary',
547
+ }}>
548
+ {translate('admin.taxRate.previewPlaceholder')}
549
+ </Typography>
550
+ )}
551
+ </Box>
552
+ </Stack>
553
+ <Divider />
554
+ <Stack direction="row" spacing={1} sx={{ px: 3, py: 2, justifyContent: 'flex-end' }}>
555
+ <Button variant="text" size="small" onClick={onPopoverClose}>
556
+ {translate('common.cancel')}
557
+ </Button>
558
+ <Button
559
+ variant="contained"
560
+ size="small"
561
+ onClick={handleConfirm}
562
+ disabled={!pendingId || pendingId === field.value}>
563
+ {translate('common.select')}
564
+ </Button>
565
+ </Stack>
566
+ </Popover>
567
+ )}
568
+ {isMobile && (
569
+ <Drawer
570
+ anchor="bottom"
571
+ open={drawerOpen}
572
+ onClose={onPopoverClose}
573
+ PaperProps={{
574
+ sx: {
575
+ borderTopLeftRadius: 16,
576
+ borderTopRightRadius: 16,
577
+ },
578
+ }}>
579
+ <Stack spacing={0} sx={{ height: '70vh', maxHeight: 'calc(100vh - 96px)' }}>
580
+ <Box sx={{ px: 2, pt: 2, pb: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
581
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
582
+ {translate('admin.taxRate.taxCode')}
583
+ </Typography>
584
+ {headerContent}
585
+ </Box>
586
+ <Divider />
587
+ {listContentMobile}
588
+ <Divider />
589
+ <Stack direction="row" spacing={1} sx={{ px: 2, py: 2, justifyContent: 'flex-end' }}>
590
+ <Button variant="text" onClick={onPopoverClose}>
591
+ {translate('common.cancel')}
592
+ </Button>
593
+ <Button variant="contained" onClick={handleConfirm} disabled={!pendingId || pendingId === field.value}>
594
+ {translate('common.select')}
595
+ </Button>
596
+ </Stack>
597
+ </Stack>
598
+ </Drawer>
599
+ )}
600
+ </>
601
+ );
602
+ }
603
+
604
+ export default function TaxCodeSelect({
605
+ name = 'tax_code',
606
+ label = '',
607
+ placeholder = '',
608
+ required = false,
609
+ disabled = false,
610
+ }: TaxCodeSelectProps) {
611
+ const { t, locale } = useLocaleContext();
612
+ const { control } = useFormContext();
613
+ const language: LocaleKey = locale.startsWith('zh') ? 'zh' : 'en';
614
+
615
+ return (
616
+ <Controller
617
+ name={name}
618
+ control={control}
619
+ rules={{ required: required ? t('common.required') : false }}
620
+ render={({ field, fieldState }) => (
621
+ <TaxCodePicker
622
+ field={field}
623
+ error={fieldState.error}
624
+ disabled={disabled}
625
+ label={label ?? t('admin.taxRate.taxCode')}
626
+ placeholder={placeholder || t('admin.taxRate.selectTaxCode')}
627
+ language={language}
628
+ translate={t}
629
+ />
630
+ )}
631
+ />
632
+ );
633
+ }