@open-mercato/core 0.6.6-develop.6385.1.9a81faa5f0 → 0.6.6-develop.6387.1.f4896ad565
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/attachments/lib/scope.js +26 -0
- package/dist/modules/attachments/lib/scope.js.map +7 -0
- package/dist/modules/catalog/commands/variants.js +4 -2
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/api/deals/map/route.js +433 -0
- package/dist/modules/customers/api/deals/map/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js +2 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.js +159 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.js +26 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js +321 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +533 -0
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/page.js +55 -0
- package/dist/modules/customers/backend/customers/deals/map/page.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/map/page.meta.js +38 -0
- package/dist/modules/customers/backend/customers/deals/map/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +22 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/create/page.js +2 -0
- package/dist/modules/customers/backend/customers/people/create/page.js.map +2 -2
- package/dist/modules/customers/components/AddressEditor.js +34 -2
- package/dist/modules/customers/components/AddressEditor.js.map +2 -2
- package/dist/modules/customers/components/AddressTiles.js +43 -3
- package/dist/modules/customers/components/AddressTiles.js.map +2 -2
- package/dist/modules/customers/components/detail/AddressesSection.js +4 -1
- package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +10 -0
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/dealsMapLocation.js +75 -0
- package/dist/modules/customers/lib/dealsMapLocation.js.map +7 -0
- package/dist/modules/messages/commands/messages.js +0 -1
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/lib/attachments.js +13 -7
- package/dist/modules/messages/lib/attachments.js.map +2 -2
- package/package.json +11 -7
- package/src/modules/attachments/lib/scope.ts +42 -0
- package/src/modules/catalog/commands/variants.ts +7 -2
- package/src/modules/customers/api/deals/map/route.ts +549 -0
- package/src/modules/customers/backend/customers/companies/create/page.tsx +2 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsLocationPanel.tsx +213 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapCanvas.tsx +79 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapCanvasImpl.tsx +407 -0
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +667 -0
- package/src/modules/customers/backend/customers/deals/map/page.meta.ts +36 -0
- package/src/modules/customers/backend/customers/deals/map/page.tsx +77 -0
- package/src/modules/customers/backend/customers/deals/pipeline/components/FilterBarRow.tsx +3 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +22 -1
- package/src/modules/customers/backend/customers/people/create/page.tsx +2 -0
- package/src/modules/customers/components/AddressEditor.tsx +35 -0
- package/src/modules/customers/components/AddressTiles.tsx +51 -1
- package/src/modules/customers/components/detail/AddressesSection.tsx +3 -0
- package/src/modules/customers/components/formConfig.tsx +10 -0
- package/src/modules/customers/data/validators.ts +13 -2
- package/src/modules/customers/i18n/de.json +24 -0
- package/src/modules/customers/i18n/en.json +24 -0
- package/src/modules/customers/i18n/es.json +24 -0
- package/src/modules/customers/i18n/pl.json +24 -0
- package/src/modules/customers/lib/dealsMapLocation.ts +125 -0
- package/src/modules/messages/commands/messages.ts +0 -1
- package/src/modules/messages/lib/attachments.ts +15 -7
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
6
|
+
import {
|
|
7
|
+
Breadcrumb,
|
|
8
|
+
BreadcrumbItem,
|
|
9
|
+
BreadcrumbLink,
|
|
10
|
+
BreadcrumbList,
|
|
11
|
+
BreadcrumbPage,
|
|
12
|
+
BreadcrumbSeparator,
|
|
13
|
+
} from '@open-mercato/ui/primitives/breadcrumb'
|
|
14
|
+
import { SearchInput } from '@open-mercato/ui/primitives/search-input'
|
|
15
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
16
|
+
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
17
|
+
import { ViewTabsRow } from '../pipeline/components/ViewTabsRow'
|
|
18
|
+
import { DealsMapView } from './components/DealsMapView'
|
|
19
|
+
|
|
20
|
+
export default function DealsMapPage(): React.ReactElement {
|
|
21
|
+
const t = useT()
|
|
22
|
+
const [search, setSearch] = React.useState('')
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Page>
|
|
26
|
+
<PageBody>
|
|
27
|
+
<div className="flex flex-col gap-2">
|
|
28
|
+
<Breadcrumb>
|
|
29
|
+
<BreadcrumbList>
|
|
30
|
+
<BreadcrumbItem>
|
|
31
|
+
<BreadcrumbLink asChild>
|
|
32
|
+
<Link href="/backend">
|
|
33
|
+
{translateWithFallback(t, 'customers.deals.kanban.breadcrumb.dashboard', 'Dashboard')}
|
|
34
|
+
</Link>
|
|
35
|
+
</BreadcrumbLink>
|
|
36
|
+
</BreadcrumbItem>
|
|
37
|
+
<BreadcrumbSeparator />
|
|
38
|
+
<BreadcrumbItem>
|
|
39
|
+
<BreadcrumbLink asChild>
|
|
40
|
+
<Link href="/backend/customers/deals">
|
|
41
|
+
{translateWithFallback(t, 'customers.deals.kanban.breadcrumb.deals', 'Deals')}
|
|
42
|
+
</Link>
|
|
43
|
+
</BreadcrumbLink>
|
|
44
|
+
</BreadcrumbItem>
|
|
45
|
+
<BreadcrumbSeparator />
|
|
46
|
+
<BreadcrumbItem>
|
|
47
|
+
<BreadcrumbPage>
|
|
48
|
+
{translateWithFallback(t, 'customers.nav.deals.map', 'Deals Map')}
|
|
49
|
+
</BreadcrumbPage>
|
|
50
|
+
</BreadcrumbItem>
|
|
51
|
+
</BreadcrumbList>
|
|
52
|
+
</Breadcrumb>
|
|
53
|
+
|
|
54
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
55
|
+
<h1 className="text-xl font-semibold leading-tight text-foreground sm:text-2xl">
|
|
56
|
+
{translateWithFallback(t, 'customers.nav.deals.map', 'Deals Map')}
|
|
57
|
+
</h1>
|
|
58
|
+
<SearchInput
|
|
59
|
+
value={search}
|
|
60
|
+
onChange={setSearch}
|
|
61
|
+
placeholder={translateWithFallback(
|
|
62
|
+
t,
|
|
63
|
+
'customers.deals.kanban.search.placeholder',
|
|
64
|
+
'Search deals…',
|
|
65
|
+
)}
|
|
66
|
+
className="w-64"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<ViewTabsRow active="map" className="mt-4" />
|
|
72
|
+
|
|
73
|
+
<DealsMapView search={search} />
|
|
74
|
+
</PageBody>
|
|
75
|
+
</Page>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -33,9 +33,9 @@ export function FilterBarRow({
|
|
|
33
33
|
// That picker is not wired up in this slice (it would need AdvancedFilterPanel integration),
|
|
34
34
|
// so the affordance is intentionally omitted instead of shipping a "coming next" toast.
|
|
35
35
|
return (
|
|
36
|
-
<div className="flex flex-
|
|
36
|
+
<div className="flex flex-col gap-2 rounded-lg bg-muted/40 px-3.5 py-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between sm:gap-y-2">
|
|
37
37
|
<div className="flex flex-wrap items-center gap-2">
|
|
38
|
-
<span className="text-overline font-bold uppercase leading-normal text-muted-foreground">
|
|
38
|
+
<span className="text-overline w-full font-bold uppercase leading-normal text-muted-foreground sm:w-auto">
|
|
39
39
|
{translateWithFallback(t, 'customers.deals.kanban.filter.label', 'Filter')}:
|
|
40
40
|
</span>
|
|
41
41
|
{leadingChips}
|
|
@@ -54,7 +54,7 @@ export function FilterBarRow({
|
|
|
54
54
|
/>
|
|
55
55
|
))}
|
|
56
56
|
</div>
|
|
57
|
-
<div className="ml-auto">{sortNode}</div>
|
|
57
|
+
<div className="sm:ml-auto">{sortNode}</div>
|
|
58
58
|
</div>
|
|
59
59
|
)
|
|
60
60
|
}
|
|
@@ -5,7 +5,7 @@ import Link from 'next/link'
|
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { translateWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
7
7
|
|
|
8
|
-
export type KanbanView = 'kanban' | 'list'
|
|
8
|
+
export type KanbanView = 'kanban' | 'list' | 'map'
|
|
9
9
|
|
|
10
10
|
type ViewTabsRowProps = {
|
|
11
11
|
active: KanbanView
|
|
@@ -14,12 +14,14 @@ type ViewTabsRowProps = {
|
|
|
14
14
|
|
|
15
15
|
const VIEW_KANBAN_HREF = '/backend/customers/deals/pipeline'
|
|
16
16
|
const VIEW_LIST_HREF = '/backend/customers/deals'
|
|
17
|
+
const VIEW_MAP_HREF = '/backend/customers/deals/map'
|
|
17
18
|
|
|
18
19
|
export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.ReactElement {
|
|
19
20
|
const t = useT()
|
|
20
21
|
const labels = {
|
|
21
22
|
kanban: translateWithFallback(t, 'customers.deals.kanban.view.kanban', 'Kanban'),
|
|
22
23
|
list: translateWithFallback(t, 'customers.deals.kanban.view.list', 'List'),
|
|
24
|
+
map: translateWithFallback(t, 'customers.deals.kanban.view.map', 'Map'),
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const baseTab =
|
|
@@ -34,6 +36,7 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
|
|
|
34
36
|
// navigate back to /pipeline.
|
|
35
37
|
const isKanbanActive = active === 'kanban'
|
|
36
38
|
const isListActive = active === 'list'
|
|
39
|
+
const isMapActive = active === 'map'
|
|
37
40
|
|
|
38
41
|
return (
|
|
39
42
|
<div
|
|
@@ -77,6 +80,24 @@ export function ViewTabsRow({ active, className }: ViewTabsRowProps): React.Reac
|
|
|
77
80
|
{labels.list}
|
|
78
81
|
</Link>
|
|
79
82
|
)}
|
|
83
|
+
{isMapActive ? (
|
|
84
|
+
<span
|
|
85
|
+
role="tab"
|
|
86
|
+
aria-selected={true}
|
|
87
|
+
className={`${baseTab} ${activeTab}`}
|
|
88
|
+
>
|
|
89
|
+
{labels.map}
|
|
90
|
+
</span>
|
|
91
|
+
) : (
|
|
92
|
+
<Link
|
|
93
|
+
href={VIEW_MAP_HREF}
|
|
94
|
+
role="tab"
|
|
95
|
+
aria-selected={false}
|
|
96
|
+
className={`${baseTab} ${inactiveTab}`}
|
|
97
|
+
>
|
|
98
|
+
{labels.map}
|
|
99
|
+
</Link>
|
|
100
|
+
)}
|
|
80
101
|
</div>
|
|
81
102
|
)
|
|
82
103
|
}
|
|
@@ -110,6 +110,8 @@ export default function CreatePersonPage() {
|
|
|
110
110
|
if (postalCode !== undefined) body.postalCode = postalCode
|
|
111
111
|
const country = normalize(entry.country)
|
|
112
112
|
if (country !== undefined) body.country = country.toUpperCase()
|
|
113
|
+
if (typeof entry.latitude === 'number') body.latitude = entry.latitude
|
|
114
|
+
if (typeof entry.longitude === 'number') body.longitude = entry.longitude
|
|
113
115
|
try {
|
|
114
116
|
await createCrud('customers/addresses', body)
|
|
115
117
|
} catch (addressErr) {
|
|
@@ -41,6 +41,8 @@ export type AddressEditorDraft = {
|
|
|
41
41
|
region: string
|
|
42
42
|
postalCode: string
|
|
43
43
|
country: string
|
|
44
|
+
latitude?: string
|
|
45
|
+
longitude?: string
|
|
44
46
|
isPrimary: boolean
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -56,6 +58,8 @@ export type AddressEditorField =
|
|
|
56
58
|
| 'region'
|
|
57
59
|
| 'postalCode'
|
|
58
60
|
| 'country'
|
|
61
|
+
| 'latitude'
|
|
62
|
+
| 'longitude'
|
|
59
63
|
| 'isPrimary'
|
|
60
64
|
|
|
61
65
|
type AddressEditorProps = {
|
|
@@ -67,6 +71,7 @@ type AddressEditorProps = {
|
|
|
67
71
|
errors?: Partial<Record<AddressEditorField, string>>
|
|
68
72
|
hidePrimaryToggle?: boolean
|
|
69
73
|
showFormatHint?: boolean
|
|
74
|
+
showCoordinateFields?: boolean
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export function AddressEditor({
|
|
@@ -78,6 +83,7 @@ export function AddressEditor({
|
|
|
78
83
|
errors = {},
|
|
79
84
|
hidePrimaryToggle = false,
|
|
80
85
|
showFormatHint = true,
|
|
86
|
+
showCoordinateFields = false,
|
|
81
87
|
}: AddressEditorProps) {
|
|
82
88
|
const pathname = usePathname()
|
|
83
89
|
const searchParams = useSearchParams()
|
|
@@ -108,6 +114,9 @@ export function AddressEditor({
|
|
|
108
114
|
region: value.region ?? '',
|
|
109
115
|
postalCode: value.postalCode ?? '',
|
|
110
116
|
country: value.country ?? '',
|
|
117
|
+
...(showCoordinateFields
|
|
118
|
+
? { latitude: value.latitude ?? '', longitude: value.longitude ?? '' }
|
|
119
|
+
: {}),
|
|
111
120
|
isPrimary: value.isPrimary ?? false,
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -437,6 +446,32 @@ export function AddressEditor({
|
|
|
437
446
|
</DialogContent>
|
|
438
447
|
</Dialog>
|
|
439
448
|
{errors.country ? <p className="text-xs text-destructive">{errors.country}</p> : null}
|
|
449
|
+
{showCoordinateFields ? (
|
|
450
|
+
<>
|
|
451
|
+
<Input
|
|
452
|
+
className={inputClass('latitude')}
|
|
453
|
+
placeholder={t('customers.people.detail.addresses.fields.latitude', 'Latitude')}
|
|
454
|
+
aria-label={t('customers.people.detail.addresses.fields.latitude', 'Latitude')}
|
|
455
|
+
inputMode="decimal"
|
|
456
|
+
value={current.latitude ?? ''}
|
|
457
|
+
onChange={(evt) => update('latitude', evt.target.value)}
|
|
458
|
+
disabled={disabled}
|
|
459
|
+
aria-invalid={errors.latitude ? 'true' : undefined}
|
|
460
|
+
/>
|
|
461
|
+
{errors.latitude ? <p className="text-xs text-destructive">{errors.latitude}</p> : null}
|
|
462
|
+
<Input
|
|
463
|
+
className={inputClass('longitude')}
|
|
464
|
+
placeholder={t('customers.people.detail.addresses.fields.longitude', 'Longitude')}
|
|
465
|
+
aria-label={t('customers.people.detail.addresses.fields.longitude', 'Longitude')}
|
|
466
|
+
inputMode="decimal"
|
|
467
|
+
value={current.longitude ?? ''}
|
|
468
|
+
onChange={(evt) => update('longitude', evt.target.value)}
|
|
469
|
+
disabled={disabled}
|
|
470
|
+
aria-invalid={errors.longitude ? 'true' : undefined}
|
|
471
|
+
/>
|
|
472
|
+
{errors.longitude ? <p className="text-xs text-destructive">{errors.longitude}</p> : null}
|
|
473
|
+
</>
|
|
474
|
+
) : null}
|
|
440
475
|
</div>
|
|
441
476
|
{!hidePrimaryToggle ? (
|
|
442
477
|
<label className="inline-flex items-center gap-2 text-sm">
|
|
@@ -8,6 +8,7 @@ import { TabEmptyState } from '@open-mercato/ui/backend/detail'
|
|
|
8
8
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
9
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
10
10
|
import { AddressView, formatAddressJson, formatAddressString, type AddressFormatStrategy } from '../utils/addressFormat'
|
|
11
|
+
import { normalizeCoordinateInput, validateCoordinateInput } from '@open-mercato/shared/lib/location/coordinates'
|
|
11
12
|
import AddressEditor from './AddressEditor'
|
|
12
13
|
import { useAddressTypes } from './detail/hooks/useAddressTypes'
|
|
13
14
|
import {
|
|
@@ -40,6 +41,8 @@ export type CustomerAddressInput = {
|
|
|
40
41
|
region?: string
|
|
41
42
|
postalCode?: string
|
|
42
43
|
country?: string
|
|
44
|
+
latitude?: number | null
|
|
45
|
+
longitude?: number | null
|
|
43
46
|
isPrimary?: boolean
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -47,6 +50,8 @@ export type CustomerAddressValue = CustomerAddressInput & {
|
|
|
47
50
|
id: string
|
|
48
51
|
purpose?: string | null
|
|
49
52
|
companyName?: string | null
|
|
53
|
+
latitude?: number | null
|
|
54
|
+
longitude?: number | null
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
type CustomerAddressTilesProps = {
|
|
@@ -62,6 +67,7 @@ type CustomerAddressTilesProps = {
|
|
|
62
67
|
onAddActionChange?: (action: { openCreateForm: () => void; addDisabled: boolean } | null) => void
|
|
63
68
|
emptyStateTitle?: string
|
|
64
69
|
emptyStateActionLabel?: string
|
|
70
|
+
showCoordinateFields?: boolean
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
type DraftAddressState = {
|
|
@@ -76,6 +82,8 @@ type DraftAddressState = {
|
|
|
76
82
|
region: string
|
|
77
83
|
postalCode: string
|
|
78
84
|
country: string
|
|
85
|
+
latitude: string
|
|
86
|
+
longitude: string
|
|
79
87
|
isPrimary: boolean
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -102,6 +110,8 @@ const defaultDraft: DraftAddressState = {
|
|
|
102
110
|
region: '',
|
|
103
111
|
postalCode: '',
|
|
104
112
|
country: '',
|
|
113
|
+
latitude: '',
|
|
114
|
+
longitude: '',
|
|
105
115
|
isPrimary: false,
|
|
106
116
|
}
|
|
107
117
|
|
|
@@ -117,6 +127,8 @@ const serverFieldMap: Record<string, DraftFieldKey> = {
|
|
|
117
127
|
region: 'region',
|
|
118
128
|
postalCode: 'postalCode',
|
|
119
129
|
country: 'country',
|
|
130
|
+
latitude: 'latitude',
|
|
131
|
+
longitude: 'longitude',
|
|
120
132
|
isPrimary: 'isPrimary',
|
|
121
133
|
}
|
|
122
134
|
|
|
@@ -173,6 +185,7 @@ export function CustomerAddressTiles({
|
|
|
173
185
|
onAddActionChange,
|
|
174
186
|
emptyStateTitle,
|
|
175
187
|
emptyStateActionLabel,
|
|
188
|
+
showCoordinateFields = false,
|
|
176
189
|
}: CustomerAddressTilesProps) {
|
|
177
190
|
const scopeVersion = useOrganizationScopeVersion()
|
|
178
191
|
const queryClient = useQueryClient()
|
|
@@ -201,6 +214,8 @@ export function CustomerAddressTiles({
|
|
|
201
214
|
region: t('customers.people.detail.addresses.fields.region'),
|
|
202
215
|
postalCode: t('customers.people.detail.addresses.fields.postalCode'),
|
|
203
216
|
country: t('customers.people.detail.addresses.fields.country'),
|
|
217
|
+
latitude: t('customers.people.detail.addresses.fields.latitude', 'Latitude'),
|
|
218
|
+
longitude: t('customers.people.detail.addresses.fields.longitude', 'Longitude'),
|
|
204
219
|
isPrimary: t('customers.people.detail.addresses.fields.primary'),
|
|
205
220
|
}),
|
|
206
221
|
[t]
|
|
@@ -281,6 +296,8 @@ export function CustomerAddressTiles({
|
|
|
281
296
|
region: address.region ?? '',
|
|
282
297
|
postalCode: address.postalCode ?? '',
|
|
283
298
|
country: address.country ? address.country.toUpperCase() : '',
|
|
299
|
+
latitude: address.latitude != null ? String(address.latitude) : '',
|
|
300
|
+
longitude: address.longitude != null ? String(address.longitude) : '',
|
|
284
301
|
isPrimary: address.isPrimary ?? false,
|
|
285
302
|
})
|
|
286
303
|
setFieldErrors({})
|
|
@@ -309,6 +326,29 @@ export function CustomerAddressTiles({
|
|
|
309
326
|
return
|
|
310
327
|
}
|
|
311
328
|
|
|
329
|
+
const coordinateErrors: Partial<Record<DraftFieldKey, string>> = {}
|
|
330
|
+
for (const key of ['latitude', 'longitude'] as const) {
|
|
331
|
+
const result = validateCoordinateInput(key, draft[key])
|
|
332
|
+
if (result.status === 'invalid') {
|
|
333
|
+
coordinateErrors[key] = t(
|
|
334
|
+
'customers.people.detail.addresses.validation.invalid',
|
|
335
|
+
undefined,
|
|
336
|
+
{ field: fieldLabels[key] }
|
|
337
|
+
)
|
|
338
|
+
} else if (result.status === 'outOfRange') {
|
|
339
|
+
coordinateErrors[key] = t(
|
|
340
|
+
'customers.people.detail.addresses.validation.coordinateRange',
|
|
341
|
+
undefined,
|
|
342
|
+
{ field: fieldLabels[key], min: result.min, max: result.max }
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (Object.keys(coordinateErrors).length) {
|
|
347
|
+
setFieldErrors((prev) => ({ ...prev, ...coordinateErrors }))
|
|
348
|
+
setGeneralError(Object.values(coordinateErrors)[0] ?? null)
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
|
|
312
352
|
const payload: CustomerAddressInput = {
|
|
313
353
|
addressLine1: trimmedLine1,
|
|
314
354
|
isPrimary: draft.isPrimary,
|
|
@@ -334,6 +374,14 @@ export function CustomerAddressTiles({
|
|
|
334
374
|
if (postal !== undefined) payload.postalCode = postal
|
|
335
375
|
const country = normalizeOptional(draft.country)
|
|
336
376
|
if (country !== undefined) payload.country = country.toUpperCase()
|
|
377
|
+
// On edit, an emptied coordinate must be sent as `null` to clear the stored value; omitting it
|
|
378
|
+
// makes the partial-update handler keep the old coordinate. On create there is nothing to clear.
|
|
379
|
+
const latitude = normalizeCoordinateInput(draft.latitude)
|
|
380
|
+
if (latitude !== undefined) payload.latitude = latitude
|
|
381
|
+
else if (editingId) payload.latitude = null
|
|
382
|
+
const longitude = normalizeCoordinateInput(draft.longitude)
|
|
383
|
+
if (longitude !== undefined) payload.longitude = longitude
|
|
384
|
+
else if (editingId) payload.longitude = null
|
|
337
385
|
|
|
338
386
|
setSaving(true)
|
|
339
387
|
setGeneralError(null)
|
|
@@ -450,7 +498,7 @@ export function CustomerAddressTiles({
|
|
|
450
498
|
<AddressEditor
|
|
451
499
|
value={draft}
|
|
452
500
|
onChange={(next) => {
|
|
453
|
-
setDraft(next)
|
|
501
|
+
setDraft({ ...next, latitude: next.latitude ?? '', longitude: next.longitude ?? '' })
|
|
454
502
|
if (Object.keys(fieldErrors).length) {
|
|
455
503
|
const nextErrors = { ...fieldErrors }
|
|
456
504
|
;(Object.keys(nextErrors) as DraftFieldKey[]).forEach((key) => {
|
|
@@ -467,6 +515,7 @@ export function CustomerAddressTiles({
|
|
|
467
515
|
disabled={disableActions}
|
|
468
516
|
errors={fieldErrors}
|
|
469
517
|
showFormatHint={!formatLoading}
|
|
518
|
+
showCoordinateFields={showCoordinateFields}
|
|
470
519
|
/>
|
|
471
520
|
{generalError ? <p className="text-xs text-status-error-text">{generalError}</p> : null}
|
|
472
521
|
<div className="flex flex-wrap justify-end gap-2">
|
|
@@ -502,6 +551,7 @@ export function CustomerAddressTiles({
|
|
|
502
551
|
handleSave,
|
|
503
552
|
generalError,
|
|
504
553
|
saving,
|
|
554
|
+
showCoordinateFields,
|
|
505
555
|
t,
|
|
506
556
|
]
|
|
507
557
|
)
|
|
@@ -95,6 +95,8 @@ export function AddressesSection({
|
|
|
95
95
|
? record.postalCode
|
|
96
96
|
: null,
|
|
97
97
|
country: typeof record.country === 'string' ? record.country : null,
|
|
98
|
+
latitude: typeof record.latitude === 'number' && Number.isFinite(record.latitude) ? record.latitude : null,
|
|
99
|
+
longitude: typeof record.longitude === 'number' && Number.isFinite(record.longitude) ? record.longitude : null,
|
|
98
100
|
isPrimary:
|
|
99
101
|
typeof record.is_primary === 'boolean'
|
|
100
102
|
? record.is_primary
|
|
@@ -242,6 +244,7 @@ export function AddressesSection({
|
|
|
242
244
|
dataAdapter={dataAdapter}
|
|
243
245
|
addressTypesAdapter={addressTypesAdapter}
|
|
244
246
|
loadFormat={loadFormat}
|
|
247
|
+
showCoordinateFields
|
|
245
248
|
/>
|
|
246
249
|
)
|
|
247
250
|
}
|
|
@@ -955,6 +955,7 @@ export const createPersonFormFields = (t: Translator): CrudField[] => {
|
|
|
955
955
|
t={t}
|
|
956
956
|
emptyLabel={t('customers.people.detail.empty.addresses')}
|
|
957
957
|
gridClassName="grid gap-4 min-[480px]:grid-cols-1 xl:grid-cols-2"
|
|
958
|
+
showCoordinateFields
|
|
958
959
|
onCreate={async (payload: CustomerAddressInput) => {
|
|
959
960
|
const nextId =
|
|
960
961
|
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
@@ -973,6 +974,8 @@ export const createPersonFormFields = (t: Translator): CrudField[] => {
|
|
|
973
974
|
region: payload.region ?? undefined,
|
|
974
975
|
postalCode: payload.postalCode ?? undefined,
|
|
975
976
|
country: payload.country ?? undefined,
|
|
977
|
+
latitude: payload.latitude ?? undefined,
|
|
978
|
+
longitude: payload.longitude ?? undefined,
|
|
976
979
|
isPrimary: payload.isPrimary ?? false,
|
|
977
980
|
}
|
|
978
981
|
const current = Array.isArray(addresses) ? addresses : []
|
|
@@ -1001,6 +1004,8 @@ export const createPersonFormFields = (t: Translator): CrudField[] => {
|
|
|
1001
1004
|
region: payload.region ?? null,
|
|
1002
1005
|
postalCode: payload.postalCode ?? null,
|
|
1003
1006
|
country: payload.country ?? null,
|
|
1007
|
+
latitude: payload.latitude ?? null,
|
|
1008
|
+
longitude: payload.longitude ?? null,
|
|
1004
1009
|
isPrimary: payload.isPrimary ?? false,
|
|
1005
1010
|
}
|
|
1006
1011
|
})
|
|
@@ -1316,6 +1321,7 @@ export const createCompanyFormFields = (t: Translator): CrudField[] => {
|
|
|
1316
1321
|
t={t}
|
|
1317
1322
|
emptyLabel={t('customers.companies.detail.empty.addresses')}
|
|
1318
1323
|
gridClassName="grid gap-4 min-[480px]:grid-cols-1 xl:grid-cols-2"
|
|
1324
|
+
showCoordinateFields
|
|
1319
1325
|
onCreate={async (payload: CustomerAddressInput) => {
|
|
1320
1326
|
const nextId =
|
|
1321
1327
|
typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
@@ -1334,6 +1340,8 @@ export const createCompanyFormFields = (t: Translator): CrudField[] => {
|
|
|
1334
1340
|
region: payload.region ?? undefined,
|
|
1335
1341
|
postalCode: payload.postalCode ?? undefined,
|
|
1336
1342
|
country: payload.country ?? undefined,
|
|
1343
|
+
latitude: payload.latitude ?? undefined,
|
|
1344
|
+
longitude: payload.longitude ?? undefined,
|
|
1337
1345
|
isPrimary: payload.isPrimary ?? false,
|
|
1338
1346
|
}
|
|
1339
1347
|
const current = Array.isArray(addresses) ? addresses : []
|
|
@@ -1362,6 +1370,8 @@ export const createCompanyFormFields = (t: Translator): CrudField[] => {
|
|
|
1362
1370
|
region: payload.region ?? null,
|
|
1363
1371
|
postalCode: payload.postalCode ?? null,
|
|
1364
1372
|
country: payload.country ?? null,
|
|
1373
|
+
latitude: payload.latitude ?? null,
|
|
1374
|
+
longitude: payload.longitude ?? null,
|
|
1365
1375
|
isPrimary: payload.isPrimary ?? false,
|
|
1366
1376
|
}
|
|
1367
1377
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { isValidPhoneNumber } from '@open-mercato/shared/lib/phone'
|
|
3
|
+
import { COORDINATE_RANGES } from '@open-mercato/shared/lib/location/coordinates'
|
|
3
4
|
import { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictionaries/lib/entrySort'
|
|
4
5
|
|
|
5
6
|
const uuid = () => z.string().uuid()
|
|
@@ -264,8 +265,18 @@ export const addressCreateSchema = scopedSchema.extend({
|
|
|
264
265
|
region: z.string().max(150).optional(),
|
|
265
266
|
postalCode: z.string().max(30).optional(),
|
|
266
267
|
country: z.string().max(150).optional(),
|
|
267
|
-
latitude: z.coerce
|
|
268
|
-
|
|
268
|
+
latitude: z.coerce
|
|
269
|
+
.number()
|
|
270
|
+
.min(COORDINATE_RANGES.latitude.min)
|
|
271
|
+
.max(COORDINATE_RANGES.latitude.max)
|
|
272
|
+
.nullable()
|
|
273
|
+
.optional(),
|
|
274
|
+
longitude: z.coerce
|
|
275
|
+
.number()
|
|
276
|
+
.min(COORDINATE_RANGES.longitude.min)
|
|
277
|
+
.max(COORDINATE_RANGES.longitude.max)
|
|
278
|
+
.nullable()
|
|
279
|
+
.optional(),
|
|
269
280
|
isPrimary: z.boolean().optional(),
|
|
270
281
|
})
|
|
271
282
|
|
|
@@ -1339,6 +1339,7 @@
|
|
|
1339
1339
|
"customers.deals.kanban.sort.title": "Sort by",
|
|
1340
1340
|
"customers.deals.kanban.view.kanban": "Kanban",
|
|
1341
1341
|
"customers.deals.kanban.view.list": "List",
|
|
1342
|
+
"customers.deals.kanban.view.map": "Karte",
|
|
1342
1343
|
"customers.deals.kanban.view.tablistLabel": "Deals views",
|
|
1343
1344
|
"customers.deals.list.actions.delete": "Löschen",
|
|
1344
1345
|
"customers.deals.list.actions.deleting": "Wird gelöscht…",
|
|
@@ -1424,6 +1425,25 @@
|
|
|
1424
1425
|
"customers.deals.list.unknownOwner": "Unbekannter Inhaber",
|
|
1425
1426
|
"customers.deals.list.unnamedCompany": "Unbenanntes Unternehmen",
|
|
1426
1427
|
"customers.deals.list.unnamedPerson": "Unbenannte Person",
|
|
1428
|
+
"customers.deals.map.canvas.label": "Deal-Karte",
|
|
1429
|
+
"customers.deals.map.canvas.loading": "Karte wird geladen…",
|
|
1430
|
+
"customers.deals.map.legend.title": "Phasen",
|
|
1431
|
+
"customers.deals.map.loadError": "Deals für die Karte konnten nicht geladen werden.",
|
|
1432
|
+
"customers.deals.map.panel.count": "{count} mit Standort",
|
|
1433
|
+
"customers.deals.map.panel.empty.description": "Füge Breiten- und Längengrad zu Firmen- oder Personenadressen hinzu, um ihre Deals hier anzuzeigen.",
|
|
1434
|
+
"customers.deals.map.panel.empty.title": "Noch keine Deals auf der Karte",
|
|
1435
|
+
"customers.deals.map.panel.hint": "Klicke auf einen Deal oder eine Kartennadel, um die Vorschau zu öffnen.",
|
|
1436
|
+
"customers.deals.map.panel.noAddress": "Keine Adresskoordinaten",
|
|
1437
|
+
"customers.deals.map.panel.sort.label": "Deal-Panel sortieren",
|
|
1438
|
+
"customers.deals.map.panel.sort.listOrder": "Listenreihenfolge",
|
|
1439
|
+
"customers.deals.map.panel.sort.proximity": "Nächstgelegene zuerst",
|
|
1440
|
+
"customers.deals.map.panel.title": "Deals mit Standort",
|
|
1441
|
+
"customers.deals.map.preview.close": "Vorschau schließen",
|
|
1442
|
+
"customers.deals.map.preview.closeShort": "Abschluss {date}",
|
|
1443
|
+
"customers.deals.map.preview.openDeal": "Deal öffnen",
|
|
1444
|
+
"customers.deals.map.preview.probabilityShort": "{value}%",
|
|
1445
|
+
"customers.deals.map.retry": "Erneut versuchen",
|
|
1446
|
+
"customers.deals.map.truncated": "Es werden die ersten {count} von {total} verorteten Deals angezeigt — verfeinere die Filter, um die Karte einzugrenzen.",
|
|
1427
1447
|
"customers.deals.pipeline.actions.openDeal": "Deal öffnen",
|
|
1428
1448
|
"customers.deals.pipeline.card.expectedClose": "Abschlussdatum",
|
|
1429
1449
|
"customers.deals.pipeline.card.probability": "Wahrscheinlichkeit",
|
|
@@ -1651,6 +1671,7 @@
|
|
|
1651
1671
|
"customers.nav.companies": "Unternehmen",
|
|
1652
1672
|
"customers.nav.companies.create": "Neues Unternehmen",
|
|
1653
1673
|
"customers.nav.deals": "Geschäfte",
|
|
1674
|
+
"customers.nav.deals.map": "Deal-Karte",
|
|
1654
1675
|
"customers.nav.deals.pipeline": "Sales-Pipeline",
|
|
1655
1676
|
"customers.nav.group": "Kunden",
|
|
1656
1677
|
"customers.nav.people": "Personen",
|
|
@@ -1766,8 +1787,10 @@
|
|
|
1766
1787
|
"customers.people.detail.addresses.fields.country": "Land",
|
|
1767
1788
|
"customers.people.detail.addresses.fields.flatNumber": "Wohnung / Einheit",
|
|
1768
1789
|
"customers.people.detail.addresses.fields.label": "Bezeichnung",
|
|
1790
|
+
"customers.people.detail.addresses.fields.latitude": "Breitengrad",
|
|
1769
1791
|
"customers.people.detail.addresses.fields.line1": "Adresszeile 1",
|
|
1770
1792
|
"customers.people.detail.addresses.fields.line2": "Adresszeile 2",
|
|
1793
|
+
"customers.people.detail.addresses.fields.longitude": "Längengrad",
|
|
1771
1794
|
"customers.people.detail.addresses.fields.postalCode": "Postleitzahl",
|
|
1772
1795
|
"customers.people.detail.addresses.fields.primary": "Als primär markieren",
|
|
1773
1796
|
"customers.people.detail.addresses.fields.region": "Bundesland / Region",
|
|
@@ -1799,6 +1822,7 @@
|
|
|
1799
1822
|
"customers.people.detail.addresses.types.saveError": "Adresstyp konnte nicht gespeichert werden.",
|
|
1800
1823
|
"customers.people.detail.addresses.update": "Adresse aktualisieren ⌘⏎ / Ctrl+Enter",
|
|
1801
1824
|
"customers.people.detail.addresses.updating": "Aktualisieren…",
|
|
1825
|
+
"customers.people.detail.addresses.validation.coordinateRange": "{{field}} muss zwischen {{min}} und {{max}} liegen",
|
|
1802
1826
|
"customers.people.detail.addresses.validation.generic": "Bitte prüfen Sie {{field}}",
|
|
1803
1827
|
"customers.people.detail.addresses.validation.invalid": "Ungültiger Wert für {{field}}",
|
|
1804
1828
|
"customers.people.detail.addresses.validation.required": "Geben Sie {{field}} ein",
|
|
@@ -1339,6 +1339,7 @@
|
|
|
1339
1339
|
"customers.deals.kanban.sort.title": "Sort by",
|
|
1340
1340
|
"customers.deals.kanban.view.kanban": "Kanban",
|
|
1341
1341
|
"customers.deals.kanban.view.list": "List",
|
|
1342
|
+
"customers.deals.kanban.view.map": "Map",
|
|
1342
1343
|
"customers.deals.kanban.view.tablistLabel": "Deals views",
|
|
1343
1344
|
"customers.deals.list.actions.delete": "Delete",
|
|
1344
1345
|
"customers.deals.list.actions.deleting": "Deleting…",
|
|
@@ -1424,6 +1425,25 @@
|
|
|
1424
1425
|
"customers.deals.list.unknownOwner": "Unknown owner",
|
|
1425
1426
|
"customers.deals.list.unnamedCompany": "Unnamed company",
|
|
1426
1427
|
"customers.deals.list.unnamedPerson": "Unnamed person",
|
|
1428
|
+
"customers.deals.map.canvas.label": "Deals map",
|
|
1429
|
+
"customers.deals.map.canvas.loading": "Loading map…",
|
|
1430
|
+
"customers.deals.map.legend.title": "Stages",
|
|
1431
|
+
"customers.deals.map.loadError": "Failed to load deals for the map.",
|
|
1432
|
+
"customers.deals.map.panel.count": "{count} located",
|
|
1433
|
+
"customers.deals.map.panel.empty.description": "Add latitude and longitude to company or person addresses to plot their deals here.",
|
|
1434
|
+
"customers.deals.map.panel.empty.title": "No deals on the map yet",
|
|
1435
|
+
"customers.deals.map.panel.hint": "Click a deal or a map pin to preview it.",
|
|
1436
|
+
"customers.deals.map.panel.noAddress": "No address coordinates",
|
|
1437
|
+
"customers.deals.map.panel.sort.label": "Sort deals panel",
|
|
1438
|
+
"customers.deals.map.panel.sort.listOrder": "List order",
|
|
1439
|
+
"customers.deals.map.panel.sort.proximity": "Closest first",
|
|
1440
|
+
"customers.deals.map.panel.title": "Deals with location",
|
|
1441
|
+
"customers.deals.map.preview.close": "Close preview",
|
|
1442
|
+
"customers.deals.map.preview.closeShort": "Close {date}",
|
|
1443
|
+
"customers.deals.map.preview.openDeal": "Open deal",
|
|
1444
|
+
"customers.deals.map.preview.probabilityShort": "{value}%",
|
|
1445
|
+
"customers.deals.map.retry": "Retry",
|
|
1446
|
+
"customers.deals.map.truncated": "Showing first {count} of {total} located deals — refine filters to narrow the map.",
|
|
1427
1447
|
"customers.deals.pipeline.actions.openDeal": "Open deal",
|
|
1428
1448
|
"customers.deals.pipeline.card.expectedClose": "Expected close",
|
|
1429
1449
|
"customers.deals.pipeline.card.probability": "Probability",
|
|
@@ -1651,6 +1671,7 @@
|
|
|
1651
1671
|
"customers.nav.companies": "Companies",
|
|
1652
1672
|
"customers.nav.companies.create": "New company",
|
|
1653
1673
|
"customers.nav.deals": "Deals",
|
|
1674
|
+
"customers.nav.deals.map": "Deals Map",
|
|
1654
1675
|
"customers.nav.deals.pipeline": "Sales Pipeline",
|
|
1655
1676
|
"customers.nav.group": "Customers",
|
|
1656
1677
|
"customers.nav.people": "People",
|
|
@@ -1766,8 +1787,10 @@
|
|
|
1766
1787
|
"customers.people.detail.addresses.fields.country": "Country",
|
|
1767
1788
|
"customers.people.detail.addresses.fields.flatNumber": "Flat / apartment",
|
|
1768
1789
|
"customers.people.detail.addresses.fields.label": "Label",
|
|
1790
|
+
"customers.people.detail.addresses.fields.latitude": "Latitude",
|
|
1769
1791
|
"customers.people.detail.addresses.fields.line1": "Address line 1",
|
|
1770
1792
|
"customers.people.detail.addresses.fields.line2": "Address line 2",
|
|
1793
|
+
"customers.people.detail.addresses.fields.longitude": "Longitude",
|
|
1771
1794
|
"customers.people.detail.addresses.fields.postalCode": "Postal code",
|
|
1772
1795
|
"customers.people.detail.addresses.fields.primary": "Set as primary",
|
|
1773
1796
|
"customers.people.detail.addresses.fields.region": "Region / State",
|
|
@@ -1799,6 +1822,7 @@
|
|
|
1799
1822
|
"customers.people.detail.addresses.types.saveError": "Failed to save address type.",
|
|
1800
1823
|
"customers.people.detail.addresses.update": "Update address ⌘⏎ / Ctrl+Enter",
|
|
1801
1824
|
"customers.people.detail.addresses.updating": "Updating…",
|
|
1825
|
+
"customers.people.detail.addresses.validation.coordinateRange": "{{field}} must be between {{min}} and {{max}}",
|
|
1802
1826
|
"customers.people.detail.addresses.validation.generic": "Check {{field}}",
|
|
1803
1827
|
"customers.people.detail.addresses.validation.invalid": "Invalid value for {{field}}",
|
|
1804
1828
|
"customers.people.detail.addresses.validation.required": "Enter {{field}}",
|