@open-mercato/core 0.7.1-develop.7122.1.421cefe668 → 0.7.1-develop.7130.1.fef2396fd8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/api/deals/aggregate/route.js +85 -0
- package/dist/modules/customers/api/deals/aggregate/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +5 -117
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -3
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +33 -9
- package/dist/modules/customers/commands/deals.js.map +3 -3
- package/dist/modules/customers/components/AddressEditor.js +71 -1
- package/dist/modules/customers/components/AddressEditor.js.map +2 -2
- package/dist/modules/customers/utils/addressFormat.js +18 -2
- package/dist/modules/customers/utils/addressFormat.js.map +2 -2
- package/dist/modules/sales/components/documents/AddressesSection.js +17 -2
- package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -24
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/normalizeAddressDraft.js +29 -0
- package/dist/modules/sales/components/documents/normalizeAddressDraft.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/customers/api/deals/aggregate/route.ts +102 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +0 -23
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +11 -172
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -1
- package/src/modules/customers/commands/deals.ts +84 -9
- package/src/modules/customers/components/AddressEditor.tsx +116 -1
- package/src/modules/customers/i18n/de.json +6 -0
- package/src/modules/customers/i18n/en.json +6 -0
- package/src/modules/customers/i18n/es.json +6 -0
- package/src/modules/customers/i18n/ko.json +6 -0
- package/src/modules/customers/i18n/pl.json +6 -0
- package/src/modules/customers/utils/addressFormat.tsx +59 -1
- package/src/modules/sales/components/documents/AddressesSection.tsx +48 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +5 -23
- package/src/modules/sales/components/documents/normalizeAddressDraft.ts +29 -0
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from '@open-mercato/ui/primitives/dialog'
|
|
25
25
|
import { buildCountryOptions } from '@open-mercato/shared/lib/location/countries'
|
|
26
26
|
import { buildHrefWithReturnTo } from '@open-mercato/shared/lib/navigation/returnTo'
|
|
27
|
-
import type
|
|
27
|
+
import { resolveTaxIdLabel, type AddressFormatStrategy } from '../utils/addressFormat'
|
|
28
28
|
import { useAddressTypes } from './detail/hooks/useAddressTypes'
|
|
29
29
|
|
|
30
30
|
type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
|
|
@@ -41,6 +41,20 @@ export type AddressEditorDraft = {
|
|
|
41
41
|
region: string
|
|
42
42
|
postalCode: string
|
|
43
43
|
country: string
|
|
44
|
+
/**
|
|
45
|
+
* Contact details that belong to the ADDRESS rather than to the customer: the tax identifier it was
|
|
46
|
+
* invoiced under, and the phone a carrier calls about a delivery. Optional so every existing caller
|
|
47
|
+
* keeps compiling; callers must opt in with the matching `show*Field` props before they render.
|
|
48
|
+
*/
|
|
49
|
+
taxId?: string
|
|
50
|
+
/**
|
|
51
|
+
* Which scheme the identifier belongs to, in Stripe's `{country}_{kind}` vocabulary. Chosen, not
|
|
52
|
+
* inferred: `PL1234567890` and `1234567890` are the same business written two ways, and a rule
|
|
53
|
+
* that reads the form of the value is guessing — the more schemes the vocabulary carries, the more
|
|
54
|
+
* often it guesses wrong, and a wrong type is worse than none because it names the number.
|
|
55
|
+
*/
|
|
56
|
+
taxIdType?: string
|
|
57
|
+
phone?: string
|
|
44
58
|
latitude?: string
|
|
45
59
|
longitude?: string
|
|
46
60
|
isPrimary: boolean
|
|
@@ -58,6 +72,9 @@ export type AddressEditorField =
|
|
|
58
72
|
| 'region'
|
|
59
73
|
| 'postalCode'
|
|
60
74
|
| 'country'
|
|
75
|
+
| 'taxId'
|
|
76
|
+
| 'taxIdType'
|
|
77
|
+
| 'phone'
|
|
61
78
|
| 'latitude'
|
|
62
79
|
| 'longitude'
|
|
63
80
|
| 'isPrimary'
|
|
@@ -72,8 +89,33 @@ type AddressEditorProps = {
|
|
|
72
89
|
hidePrimaryToggle?: boolean
|
|
73
90
|
showFormatHint?: boolean
|
|
74
91
|
showCoordinateFields?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* Render the phone. Off by default, opt-in for the same reason `showCoordinateFields` is: only a
|
|
94
|
+
* caller whose storage can hold a field should offer it.
|
|
95
|
+
*/
|
|
96
|
+
showPhoneField?: boolean
|
|
97
|
+
/**
|
|
98
|
+
* Render the tax identifier and its scheme. Separate from the phone, and not for symmetry — a
|
|
99
|
+
* phone is a contact detail and a tax identifier is not, and the two stop travelling together at
|
|
100
|
+
* the next phase: `CustomerAddress` gains a `phone` column and no tax id, so the address book will
|
|
101
|
+
* offer one and not the other.
|
|
102
|
+
*
|
|
103
|
+
* Both gate the CALLER, not the field. Inside a tile that opts in, the input renders whether or not
|
|
104
|
+
* it carries a value and takes the same `disabled` as every neighbour.
|
|
105
|
+
*/
|
|
106
|
+
showTaxIdField?: boolean
|
|
75
107
|
}
|
|
76
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The schemes offered in the picker, in the order a Polish deployment meets them. The vocabulary is
|
|
111
|
+
* additive under the backward-compatibility contract, so a new scheme is a new entry here rather
|
|
112
|
+
* than a new branch in a rule that has to guess.
|
|
113
|
+
*
|
|
114
|
+
* The labels are the ones `resolveTaxIdLabel` resolves, so the picker and the marker beside the
|
|
115
|
+
* filled field always read the same.
|
|
116
|
+
*/
|
|
117
|
+
const TAX_ID_TYPES = ['pl_nip', 'eu_vat', 'other'] as const
|
|
118
|
+
|
|
77
119
|
export function AddressEditor({
|
|
78
120
|
value,
|
|
79
121
|
onChange,
|
|
@@ -84,6 +126,8 @@ export function AddressEditor({
|
|
|
84
126
|
hidePrimaryToggle = false,
|
|
85
127
|
showFormatHint = true,
|
|
86
128
|
showCoordinateFields = false,
|
|
129
|
+
showPhoneField = false,
|
|
130
|
+
showTaxIdField = false,
|
|
87
131
|
}: AddressEditorProps) {
|
|
88
132
|
const pathname = usePathname()
|
|
89
133
|
const searchParams = useSearchParams()
|
|
@@ -102,6 +146,11 @@ export function AddressEditor({
|
|
|
102
146
|
[t],
|
|
103
147
|
)
|
|
104
148
|
|
|
149
|
+
const taxIdLabels = {
|
|
150
|
+
plNip: t('customers.people.detail.addresses.fields.taxId.plNip', 'Tax ID'),
|
|
151
|
+
euVat: t('customers.people.detail.addresses.fields.taxId.euVat', 'EU VAT'),
|
|
152
|
+
other: t('customers.people.detail.addresses.fields.taxId.other', 'Tax number'),
|
|
153
|
+
}
|
|
105
154
|
const current: AddressEditorDraft = {
|
|
106
155
|
name: value.name ?? '',
|
|
107
156
|
purpose: value.purpose ?? '',
|
|
@@ -114,6 +163,9 @@ export function AddressEditor({
|
|
|
114
163
|
region: value.region ?? '',
|
|
115
164
|
postalCode: value.postalCode ?? '',
|
|
116
165
|
country: value.country ?? '',
|
|
166
|
+
taxId: value.taxId ?? '',
|
|
167
|
+
taxIdType: value.taxIdType ?? '',
|
|
168
|
+
phone: value.phone ?? '',
|
|
117
169
|
...(showCoordinateFields
|
|
118
170
|
? { latitude: value.latitude ?? '', longitude: value.longitude ?? '' }
|
|
119
171
|
: {}),
|
|
@@ -472,6 +524,69 @@ export function AddressEditor({
|
|
|
472
524
|
{errors.longitude ? <p className="text-xs text-destructive">{errors.longitude}</p> : null}
|
|
473
525
|
</>
|
|
474
526
|
) : null}
|
|
527
|
+
{showTaxIdField ? (
|
|
528
|
+
<>
|
|
529
|
+
<Select
|
|
530
|
+
value={current.taxIdType || undefined}
|
|
531
|
+
onValueChange={(next) => update('taxIdType', next ?? '')}
|
|
532
|
+
disabled={disabled}
|
|
533
|
+
>
|
|
534
|
+
<SelectTrigger
|
|
535
|
+
className={errors.taxIdType ? 'border-destructive' : undefined}
|
|
536
|
+
aria-label={t('customers.people.detail.addresses.fields.taxIdType', 'Tax id type')}
|
|
537
|
+
aria-invalid={errors.taxIdType ? 'true' : undefined}
|
|
538
|
+
>
|
|
539
|
+
<SelectValue
|
|
540
|
+
placeholder={t('customers.people.detail.addresses.fields.taxIdType', 'Tax id type')}
|
|
541
|
+
/>
|
|
542
|
+
</SelectTrigger>
|
|
543
|
+
<SelectContent>
|
|
544
|
+
{TAX_ID_TYPES.map((type) => (
|
|
545
|
+
<SelectItem key={type} value={type}>
|
|
546
|
+
{resolveTaxIdLabel(taxIdLabels, type)}
|
|
547
|
+
</SelectItem>
|
|
548
|
+
))}
|
|
549
|
+
</SelectContent>
|
|
550
|
+
</Select>
|
|
551
|
+
<Input
|
|
552
|
+
className={inputClass('taxId')}
|
|
553
|
+
placeholder={t('customers.people.detail.addresses.fields.taxId', 'Tax number')}
|
|
554
|
+
aria-label={t('customers.people.detail.addresses.fields.taxId', 'Tax number')}
|
|
555
|
+
rightIcon={
|
|
556
|
+
current.taxId ? (
|
|
557
|
+
<span className="text-xs">{resolveTaxIdLabel(taxIdLabels, current.taxIdType)}</span>
|
|
558
|
+
) : null
|
|
559
|
+
}
|
|
560
|
+
value={current.taxId ?? ''}
|
|
561
|
+
onChange={(evt) => update('taxId', evt.target.value)}
|
|
562
|
+
disabled={disabled}
|
|
563
|
+
aria-invalid={errors.taxId ? 'true' : undefined}
|
|
564
|
+
/>
|
|
565
|
+
{errors.taxId ? <p className="text-xs text-destructive">{errors.taxId}</p> : null}
|
|
566
|
+
</>
|
|
567
|
+
) : null}
|
|
568
|
+
{showPhoneField ? (
|
|
569
|
+
<>
|
|
570
|
+
<Input
|
|
571
|
+
className={inputClass('phone')}
|
|
572
|
+
placeholder={t('customers.people.detail.addresses.fields.phone', 'Phone')}
|
|
573
|
+
aria-label={t('customers.people.detail.addresses.fields.phone', 'Phone')}
|
|
574
|
+
rightIcon={
|
|
575
|
+
current.phone ? (
|
|
576
|
+
<span className="text-xs">
|
|
577
|
+
{t('customers.people.detail.addresses.fields.phone', 'Phone')}
|
|
578
|
+
</span>
|
|
579
|
+
) : null
|
|
580
|
+
}
|
|
581
|
+
inputMode="tel"
|
|
582
|
+
value={current.phone ?? ''}
|
|
583
|
+
onChange={(evt) => update('phone', evt.target.value)}
|
|
584
|
+
disabled={disabled}
|
|
585
|
+
aria-invalid={errors.phone ? 'true' : undefined}
|
|
586
|
+
/>
|
|
587
|
+
{errors.phone ? <p className="text-xs text-destructive">{errors.phone}</p> : null}
|
|
588
|
+
</>
|
|
589
|
+
) : null}
|
|
475
590
|
</div>
|
|
476
591
|
{!hidePrimaryToggle ? (
|
|
477
592
|
<label className="inline-flex items-center gap-2 text-sm">
|
|
@@ -1867,11 +1867,17 @@
|
|
|
1867
1867
|
"customers.people.detail.addresses.fields.line1": "Adresszeile 1",
|
|
1868
1868
|
"customers.people.detail.addresses.fields.line2": "Adresszeile 2",
|
|
1869
1869
|
"customers.people.detail.addresses.fields.longitude": "Längengrad",
|
|
1870
|
+
"customers.people.detail.addresses.fields.phone": "Telefon",
|
|
1870
1871
|
"customers.people.detail.addresses.fields.postalCode": "Postleitzahl",
|
|
1871
1872
|
"customers.people.detail.addresses.fields.primary": "Als primär markieren",
|
|
1872
1873
|
"customers.people.detail.addresses.fields.region": "Bundesland / Region",
|
|
1873
1874
|
"customers.people.detail.addresses.fields.street": "Straße",
|
|
1874
1875
|
"customers.people.detail.addresses.fields.streetExtra": "Weitere Adressangaben",
|
|
1876
|
+
"customers.people.detail.addresses.fields.taxId": "Steuernummer",
|
|
1877
|
+
"customers.people.detail.addresses.fields.taxId.euVat": "USt-IdNr.",
|
|
1878
|
+
"customers.people.detail.addresses.fields.taxId.other": "Steuerliche Nummer",
|
|
1879
|
+
"customers.people.detail.addresses.fields.taxId.plNip": "Steuernummer",
|
|
1880
|
+
"customers.people.detail.addresses.fields.taxIdType": "Art der Steuernummer",
|
|
1875
1881
|
"customers.people.detail.addresses.fields.type": "Adresstyp",
|
|
1876
1882
|
"customers.people.detail.addresses.formatLoadError": "Adresskonfiguration konnte nicht geladen werden",
|
|
1877
1883
|
"customers.people.detail.addresses.formatLoading": "Adresspräferenzen werden geladen…",
|
|
@@ -1867,11 +1867,17 @@
|
|
|
1867
1867
|
"customers.people.detail.addresses.fields.line1": "Address line 1",
|
|
1868
1868
|
"customers.people.detail.addresses.fields.line2": "Address line 2",
|
|
1869
1869
|
"customers.people.detail.addresses.fields.longitude": "Longitude",
|
|
1870
|
+
"customers.people.detail.addresses.fields.phone": "Phone",
|
|
1870
1871
|
"customers.people.detail.addresses.fields.postalCode": "Postal code",
|
|
1871
1872
|
"customers.people.detail.addresses.fields.primary": "Set as primary",
|
|
1872
1873
|
"customers.people.detail.addresses.fields.region": "Region / State",
|
|
1873
1874
|
"customers.people.detail.addresses.fields.street": "Street",
|
|
1874
1875
|
"customers.people.detail.addresses.fields.streetExtra": "Additional address details",
|
|
1876
|
+
"customers.people.detail.addresses.fields.taxId": "Tax number",
|
|
1877
|
+
"customers.people.detail.addresses.fields.taxId.euVat": "EU VAT",
|
|
1878
|
+
"customers.people.detail.addresses.fields.taxId.other": "Tax number",
|
|
1879
|
+
"customers.people.detail.addresses.fields.taxId.plNip": "Tax ID",
|
|
1880
|
+
"customers.people.detail.addresses.fields.taxIdType": "Tax id type",
|
|
1875
1881
|
"customers.people.detail.addresses.fields.type": "Address type",
|
|
1876
1882
|
"customers.people.detail.addresses.formatLoadError": "Failed to load address configuration",
|
|
1877
1883
|
"customers.people.detail.addresses.formatLoading": "Loading address preferences…",
|
|
@@ -1867,11 +1867,17 @@
|
|
|
1867
1867
|
"customers.people.detail.addresses.fields.line1": "Dirección (línea 1)",
|
|
1868
1868
|
"customers.people.detail.addresses.fields.line2": "Dirección (línea 2)",
|
|
1869
1869
|
"customers.people.detail.addresses.fields.longitude": "Longitud",
|
|
1870
|
+
"customers.people.detail.addresses.fields.phone": "Teléfono",
|
|
1870
1871
|
"customers.people.detail.addresses.fields.postalCode": "Código postal",
|
|
1871
1872
|
"customers.people.detail.addresses.fields.primary": "Marcar como principal",
|
|
1872
1873
|
"customers.people.detail.addresses.fields.region": "Provincia / región",
|
|
1873
1874
|
"customers.people.detail.addresses.fields.street": "Calle",
|
|
1874
1875
|
"customers.people.detail.addresses.fields.streetExtra": "Detalles adicionales de la dirección",
|
|
1876
|
+
"customers.people.detail.addresses.fields.taxId": "Número fiscal",
|
|
1877
|
+
"customers.people.detail.addresses.fields.taxId.euVat": "IVA UE",
|
|
1878
|
+
"customers.people.detail.addresses.fields.taxId.other": "Número fiscal",
|
|
1879
|
+
"customers.people.detail.addresses.fields.taxId.plNip": "NIF",
|
|
1880
|
+
"customers.people.detail.addresses.fields.taxIdType": "Tipo de número fiscal",
|
|
1875
1881
|
"customers.people.detail.addresses.fields.type": "Tipo de dirección",
|
|
1876
1882
|
"customers.people.detail.addresses.formatLoadError": "No se pudo cargar la configuración de la dirección",
|
|
1877
1883
|
"customers.people.detail.addresses.formatLoading": "Cargando preferencias de dirección…",
|
|
@@ -1867,11 +1867,17 @@
|
|
|
1867
1867
|
"customers.people.detail.addresses.fields.line1": "주소 1",
|
|
1868
1868
|
"customers.people.detail.addresses.fields.line2": "주소 2",
|
|
1869
1869
|
"customers.people.detail.addresses.fields.longitude": "경도",
|
|
1870
|
+
"customers.people.detail.addresses.fields.phone": "전화번호",
|
|
1870
1871
|
"customers.people.detail.addresses.fields.postalCode": "우편번호",
|
|
1871
1872
|
"customers.people.detail.addresses.fields.primary": "기본 주소로 설정",
|
|
1872
1873
|
"customers.people.detail.addresses.fields.region": "지역 / 주",
|
|
1873
1874
|
"customers.people.detail.addresses.fields.street": "도로명",
|
|
1874
1875
|
"customers.people.detail.addresses.fields.streetExtra": "추가 주소 정보",
|
|
1876
|
+
"customers.people.detail.addresses.fields.taxId": "세금 번호",
|
|
1877
|
+
"customers.people.detail.addresses.fields.taxId.euVat": "EU 부가세 번호",
|
|
1878
|
+
"customers.people.detail.addresses.fields.taxId.other": "세금 번호",
|
|
1879
|
+
"customers.people.detail.addresses.fields.taxId.plNip": "납세자 번호",
|
|
1880
|
+
"customers.people.detail.addresses.fields.taxIdType": "세금 번호 유형",
|
|
1875
1881
|
"customers.people.detail.addresses.fields.type": "주소 유형",
|
|
1876
1882
|
"customers.people.detail.addresses.formatLoadError": "주소 설정을 불러오지 못했습니다",
|
|
1877
1883
|
"customers.people.detail.addresses.formatLoading": "주소 설정을 불러오는 중…",
|
|
@@ -1867,11 +1867,17 @@
|
|
|
1867
1867
|
"customers.people.detail.addresses.fields.line1": "Adres (linia 1)",
|
|
1868
1868
|
"customers.people.detail.addresses.fields.line2": "Adres (linia 2)",
|
|
1869
1869
|
"customers.people.detail.addresses.fields.longitude": "Długość geograficzna",
|
|
1870
|
+
"customers.people.detail.addresses.fields.phone": "Telefon",
|
|
1870
1871
|
"customers.people.detail.addresses.fields.postalCode": "Kod pocztowy",
|
|
1871
1872
|
"customers.people.detail.addresses.fields.primary": "Ustaw jako główny",
|
|
1872
1873
|
"customers.people.detail.addresses.fields.region": "Region / województwo",
|
|
1873
1874
|
"customers.people.detail.addresses.fields.street": "Ulica",
|
|
1874
1875
|
"customers.people.detail.addresses.fields.streetExtra": "Dodatkowe informacje adresowe",
|
|
1876
|
+
"customers.people.detail.addresses.fields.taxId": "Nr podatkowy",
|
|
1877
|
+
"customers.people.detail.addresses.fields.taxId.euVat": "VAT UE",
|
|
1878
|
+
"customers.people.detail.addresses.fields.taxId.other": "Nr podatkowy",
|
|
1879
|
+
"customers.people.detail.addresses.fields.taxId.plNip": "NIP",
|
|
1880
|
+
"customers.people.detail.addresses.fields.taxIdType": "Typ numeru",
|
|
1875
1881
|
"customers.people.detail.addresses.fields.type": "Typ adresu",
|
|
1876
1882
|
"customers.people.detail.addresses.formatLoadError": "Nie udało się wczytać konfiguracji adresów",
|
|
1877
1883
|
"customers.people.detail.addresses.formatLoading": "Ładowanie preferencji adresowych…",
|
|
@@ -13,6 +13,34 @@ export type AddressValue = {
|
|
|
13
13
|
postalCode?: string | null
|
|
14
14
|
country?: string | null
|
|
15
15
|
companyName?: string | null
|
|
16
|
+
/**
|
|
17
|
+
* Contact details that belong to the ADDRESS rather than to the customer: who to call about this
|
|
18
|
+
* delivery, and the tax identifier this invoice address was billed under. They remain available to
|
|
19
|
+
* address editors and snapshot payloads, but are deliberately excluded from `formatAddressLines`
|
|
20
|
+
* and `AddressView`, whose existing contract remains postal-only.
|
|
21
|
+
*
|
|
22
|
+
* `taxIdType` interprets the value in Stripe's `{country}_{kind}` vocabulary (`pl_nip`, `eu_vat`,
|
|
23
|
+
* `other`, widened additively): `1234567890` and `PL1234567890` are the same business, and only the
|
|
24
|
+
* type tells a domestic identifier from an EU VAT number. It is metadata about `taxId`, never a
|
|
25
|
+
* displayed field of its own.
|
|
26
|
+
*/
|
|
27
|
+
phone?: string | null
|
|
28
|
+
taxId?: string | null
|
|
29
|
+
taxIdType?: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tax-id labels keyed by `taxIdType`, for a caller that wants the identifier named correctly rather
|
|
34
|
+
* than generically.
|
|
35
|
+
*
|
|
36
|
+
* The stored scheme is chosen explicitly rather than inferred from the identifier. `other` also
|
|
37
|
+
* covers an address written before `taxIdType` existed. An unrecognised type takes the `other` route
|
|
38
|
+
* instead of guessing a domestic scheme.
|
|
39
|
+
*/
|
|
40
|
+
export type TaxIdLabelByType = {
|
|
41
|
+
plNip: string
|
|
42
|
+
euVat: string
|
|
43
|
+
other: string
|
|
16
44
|
}
|
|
17
45
|
|
|
18
46
|
export type AddressJsonShape = {
|
|
@@ -100,6 +128,31 @@ export function formatAddressString(address: AddressValue, format: AddressFormat
|
|
|
100
128
|
return formatAddressLines(address, format).filter(Boolean).join(separator)
|
|
101
129
|
}
|
|
102
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Which member of a label map names which scheme. Private, and deliberately not exhaustive: an
|
|
133
|
+
* unrecognised type resolves to `other`, so the vocabulary can widen without every caller being
|
|
134
|
+
* updated in the same release.
|
|
135
|
+
*/
|
|
136
|
+
const TAX_ID_LABEL_KEY_BY_TYPE: Record<string, keyof TaxIdLabelByType> = {
|
|
137
|
+
pl_nip: 'plNip',
|
|
138
|
+
eu_vat: 'euVat',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The label a tax identifier should carry, given its type. Exported because the editor renders the
|
|
143
|
+
* same identifier as an input and must name it the same way this formatter does — two copies of the
|
|
144
|
+
* mapping is exactly how a foreign number ends up under a domestic scheme's name.
|
|
145
|
+
*/
|
|
146
|
+
export function resolveTaxIdLabel(
|
|
147
|
+
label: string | TaxIdLabelByType | undefined,
|
|
148
|
+
taxIdType: string | null | undefined,
|
|
149
|
+
): string | undefined {
|
|
150
|
+
if (!label) return undefined
|
|
151
|
+
if (typeof label === 'string') return label
|
|
152
|
+
const key = TAX_ID_LABEL_KEY_BY_TYPE[typeof taxIdType === 'string' ? taxIdType : ''] ?? 'other'
|
|
153
|
+
return label[key]
|
|
154
|
+
}
|
|
155
|
+
|
|
103
156
|
type AddressViewProps = {
|
|
104
157
|
address: AddressValue
|
|
105
158
|
format: AddressFormatStrategy
|
|
@@ -107,7 +160,12 @@ type AddressViewProps = {
|
|
|
107
160
|
lineClassName?: string
|
|
108
161
|
}
|
|
109
162
|
|
|
110
|
-
export function AddressView({
|
|
163
|
+
export function AddressView({
|
|
164
|
+
address,
|
|
165
|
+
format,
|
|
166
|
+
className,
|
|
167
|
+
lineClassName,
|
|
168
|
+
}: AddressViewProps): React.ReactElement | null {
|
|
111
169
|
const lines = formatAddressLines(address, format)
|
|
112
170
|
if (!lines.length) return null
|
|
113
171
|
return (
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
|
|
3
1
|
"use client"
|
|
4
2
|
|
|
5
3
|
import * as React from 'react'
|
|
@@ -72,17 +70,47 @@ const emptyDraft: AddressEditorDraft = {
|
|
|
72
70
|
region: '',
|
|
73
71
|
postalCode: '',
|
|
74
72
|
country: '',
|
|
73
|
+
taxId: '',
|
|
74
|
+
taxIdType: '',
|
|
75
|
+
phone: '',
|
|
75
76
|
isPrimary: false,
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const EDITABLE_SNAPSHOT_KEYS = new Set(Object.keys(emptyDraft))
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* The fields the editor owns, typed, plus whatever else the caller's snapshot carried.
|
|
83
|
+
*
|
|
84
|
+
* The index signature is what lets an integration's extra keys — anything a future
|
|
85
|
+
* integration writes — survive the merge-back below, while the named fields stay `string` for callers
|
|
86
|
+
* that read them. `taxId` and `phone` are NOT among them any more: the editor renders both, so they
|
|
87
|
+
* are assigned from the draft like every other field. Reading
|
|
88
|
+
* `normalized.city` off a bare `Record<string, unknown>` yields `unknown`, which is what forced the
|
|
89
|
+
* `@ts-nocheck` on this file.
|
|
90
|
+
*/
|
|
91
|
+
type NormalizedAddressDraft = {
|
|
92
|
+
name?: string
|
|
93
|
+
purpose?: string
|
|
94
|
+
companyName?: string
|
|
95
|
+
addressLine1?: string
|
|
96
|
+
addressLine2?: string
|
|
97
|
+
buildingNumber?: string
|
|
98
|
+
flatNumber?: string
|
|
99
|
+
city?: string
|
|
100
|
+
region?: string
|
|
101
|
+
postalCode?: string
|
|
102
|
+
country?: string
|
|
103
|
+
taxId?: string
|
|
104
|
+
phone?: string
|
|
105
|
+
isPrimary?: boolean
|
|
106
|
+
} & Record<string, unknown>
|
|
107
|
+
|
|
80
108
|
function normalizeAddressDraft(
|
|
81
109
|
draft?: AddressEditorDraft | null,
|
|
82
110
|
previous?: Record<string, unknown> | null,
|
|
83
|
-
):
|
|
111
|
+
): NormalizedAddressDraft | null {
|
|
84
112
|
if (!draft) return null
|
|
85
|
-
const normalized:
|
|
113
|
+
const normalized: NormalizedAddressDraft = {}
|
|
86
114
|
let hasEditableContent = false
|
|
87
115
|
const assign = (key: keyof AddressEditorDraft, target: string) => {
|
|
88
116
|
const value = draft[key]
|
|
@@ -103,6 +131,9 @@ function normalizeAddressDraft(
|
|
|
103
131
|
assign('region', 'region')
|
|
104
132
|
assign('postalCode', 'postalCode')
|
|
105
133
|
assign('country', 'country')
|
|
134
|
+
assign('taxId', 'taxId')
|
|
135
|
+
assign('taxIdType', 'taxIdType')
|
|
136
|
+
assign('phone', 'phone')
|
|
106
137
|
assign('isPrimary', 'isPrimary')
|
|
107
138
|
if (!hasEditableContent) return null
|
|
108
139
|
if (previous) {
|
|
@@ -129,6 +160,9 @@ function draftFromSnapshot(snapshot?: Record<string, unknown> | null): AddressEd
|
|
|
129
160
|
region: typeof record.region === 'string' ? record.region : '',
|
|
130
161
|
postalCode: typeof record.postalCode === 'string' ? record.postalCode : '',
|
|
131
162
|
country: typeof record.country === 'string' ? record.country : '',
|
|
163
|
+
taxId: typeof record.taxId === 'string' ? record.taxId : '',
|
|
164
|
+
taxIdType: typeof record.taxIdType === 'string' ? record.taxIdType : '',
|
|
165
|
+
phone: typeof record.phone === 'string' ? record.phone : '',
|
|
132
166
|
isPrimary: record.isPrimary === true,
|
|
133
167
|
}
|
|
134
168
|
}
|
|
@@ -370,7 +404,10 @@ export function SalesDocumentAddressesSection({
|
|
|
370
404
|
companyName: value.companyName ?? null,
|
|
371
405
|
}
|
|
372
406
|
})
|
|
373
|
-
.
|
|
407
|
+
// Narrow to non-null only. Asserting `DocumentAddressAssignment` here was wrong: its
|
|
408
|
+
// optional fields (`name?`, `purpose?`, …) admit `undefined`, which the mapped literal's
|
|
409
|
+
// `string | null` does not, so the predicate was not assignable to what it filtered.
|
|
410
|
+
.filter((entry): entry is NonNullable<typeof entry> => entry !== null)
|
|
374
411
|
setDocumentAddresses(mapped)
|
|
375
412
|
} else {
|
|
376
413
|
setDocumentAddresses([])
|
|
@@ -1093,9 +1130,12 @@ export function SalesDocumentAddressesSection({
|
|
|
1093
1130
|
<AddressEditor
|
|
1094
1131
|
value={shippingDraft}
|
|
1095
1132
|
format={addressFormat}
|
|
1133
|
+
showPhoneField
|
|
1134
|
+
showTaxIdField
|
|
1096
1135
|
t={t as Translator}
|
|
1097
1136
|
onChange={(next) => setShippingDraft(next)}
|
|
1098
1137
|
hidePrimaryToggle
|
|
1138
|
+
disabled={locked}
|
|
1099
1139
|
/>
|
|
1100
1140
|
<SwitchField
|
|
1101
1141
|
label={t('sales.documents.form.address.saveToCustomer', 'Save this address to the customer')}
|
|
@@ -1160,9 +1200,12 @@ export function SalesDocumentAddressesSection({
|
|
|
1160
1200
|
<AddressEditor
|
|
1161
1201
|
value={billingDraft}
|
|
1162
1202
|
format={addressFormat}
|
|
1203
|
+
showPhoneField
|
|
1204
|
+
showTaxIdField
|
|
1163
1205
|
t={t as Translator}
|
|
1164
1206
|
onChange={(next) => setBillingDraft(next)}
|
|
1165
1207
|
hidePrimaryToggle
|
|
1208
|
+
disabled={locked}
|
|
1166
1209
|
/>
|
|
1167
1210
|
<SwitchField
|
|
1168
1211
|
label={t('sales.documents.form.address.saveToCustomer', 'Save this address to the customer')}
|
|
@@ -60,6 +60,7 @@ import { AddressEditor, type AddressEditorDraft } from '@open-mercato/core/modul
|
|
|
60
60
|
import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
|
|
61
61
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
62
62
|
import { SalesOrderDraftLines, createSalesOrderLineDraft, type SalesOrderLineDraft } from './SalesOrderDraftLines'
|
|
63
|
+
import { normalizeAddressDraft } from './normalizeAddressDraft'
|
|
63
64
|
|
|
64
65
|
const logger = createLogger('sales')
|
|
65
66
|
|
|
@@ -435,29 +436,6 @@ function parseCustomerOptions(items: unknown[], kind: 'person' | 'company'): Cus
|
|
|
435
436
|
return parsed
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
function normalizeAddressDraft(draft?: AddressDraft | null): Record<string, unknown> | null {
|
|
439
|
-
if (!draft) return null
|
|
440
|
-
const normalized: Record<string, unknown> = {}
|
|
441
|
-
const assign = (key: keyof AddressDraft, target: string) => {
|
|
442
|
-
const value = draft[key]
|
|
443
|
-
if (typeof value === 'string' && value.trim().length) normalized[target] = value.trim()
|
|
444
|
-
if (typeof value === 'boolean') normalized[target] = value
|
|
445
|
-
}
|
|
446
|
-
assign('name', 'name')
|
|
447
|
-
assign('purpose', 'purpose')
|
|
448
|
-
assign('companyName', 'companyName')
|
|
449
|
-
assign('addressLine1', 'addressLine1')
|
|
450
|
-
assign('addressLine2', 'addressLine2')
|
|
451
|
-
assign('buildingNumber', 'buildingNumber')
|
|
452
|
-
assign('flatNumber', 'flatNumber')
|
|
453
|
-
assign('city', 'city')
|
|
454
|
-
assign('region', 'region')
|
|
455
|
-
assign('postalCode', 'postalCode')
|
|
456
|
-
assign('country', 'country')
|
|
457
|
-
assign('isPrimary', 'isPrimary')
|
|
458
|
-
return Object.keys(normalized).length ? normalized : null
|
|
459
|
-
}
|
|
460
|
-
|
|
461
439
|
type DocumentNumberFieldProps = CrudCustomFieldRenderProps & { t: Translator }
|
|
462
440
|
|
|
463
441
|
type BillingAddressSectionFieldProps = CrudCustomFieldRenderProps & {
|
|
@@ -655,6 +633,8 @@ function BillingAddressSectionField({ values, setFormValue, t, addressesLoading,
|
|
|
655
633
|
t={t}
|
|
656
634
|
onChange={(next) => updateValue('billingAddressDraft', next)}
|
|
657
635
|
hidePrimaryToggle
|
|
636
|
+
showPhoneField
|
|
637
|
+
showTaxIdField
|
|
658
638
|
/>
|
|
659
639
|
<SwitchField
|
|
660
640
|
containerClassName="col-span-2"
|
|
@@ -1267,6 +1247,8 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
|
|
|
1267
1247
|
t={t}
|
|
1268
1248
|
onChange={(next) => updateValue('shippingAddressDraft', next)}
|
|
1269
1249
|
hidePrimaryToggle
|
|
1250
|
+
showPhoneField
|
|
1251
|
+
showTaxIdField
|
|
1270
1252
|
/>
|
|
1271
1253
|
<SwitchField
|
|
1272
1254
|
containerClassName="col-span-2"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AddressEditorDraft } from '@open-mercato/core/modules/customers/components/AddressEditor'
|
|
2
|
+
|
|
3
|
+
export function normalizeAddressDraft(
|
|
4
|
+
draft?: AddressEditorDraft | null,
|
|
5
|
+
): Record<string, unknown> | null {
|
|
6
|
+
if (!draft) return null
|
|
7
|
+
const normalized: Record<string, unknown> = {}
|
|
8
|
+
const assign = (key: keyof AddressEditorDraft, target: string) => {
|
|
9
|
+
const value = draft[key]
|
|
10
|
+
if (typeof value === 'string' && value.trim().length) normalized[target] = value.trim()
|
|
11
|
+
if (typeof value === 'boolean') normalized[target] = value
|
|
12
|
+
}
|
|
13
|
+
assign('name', 'name')
|
|
14
|
+
assign('purpose', 'purpose')
|
|
15
|
+
assign('companyName', 'companyName')
|
|
16
|
+
assign('addressLine1', 'addressLine1')
|
|
17
|
+
assign('addressLine2', 'addressLine2')
|
|
18
|
+
assign('buildingNumber', 'buildingNumber')
|
|
19
|
+
assign('flatNumber', 'flatNumber')
|
|
20
|
+
assign('city', 'city')
|
|
21
|
+
assign('region', 'region')
|
|
22
|
+
assign('postalCode', 'postalCode')
|
|
23
|
+
assign('country', 'country')
|
|
24
|
+
assign('taxId', 'taxId')
|
|
25
|
+
assign('taxIdType', 'taxIdType')
|
|
26
|
+
assign('phone', 'phone')
|
|
27
|
+
assign('isPrimary', 'isPrimary')
|
|
28
|
+
return Object.keys(normalized).length ? normalized : null
|
|
29
|
+
}
|