@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,472 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { Plus, Settings } from 'lucide-react'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { Input } from '@open-mercato/ui/primitives/input'
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ DialogTrigger,
16
+ } from '@open-mercato/ui/primitives/dialog'
17
+ import { buildCountryOptions } from '@open-mercato/shared/lib/location/countries'
18
+ import type { AddressFormatStrategy } from './addressFormat'
19
+
20
+ type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
21
+
22
+ export type AddressTypeOption = {
23
+ value: string
24
+ label: string
25
+ }
26
+
27
+ export type AddressTypesAdapter<C = unknown> = {
28
+ list: (context?: C) => Promise<AddressTypeOption[]>
29
+ create?: (value: string, context?: C) => Promise<AddressTypeOption | null>
30
+ manageHref?: string
31
+ }
32
+
33
+ export type AddressEditorDraft = {
34
+ name: string
35
+ purpose: string
36
+ companyName: string
37
+ addressLine1: string
38
+ addressLine2: string
39
+ buildingNumber: string
40
+ flatNumber: string
41
+ city: string
42
+ region: string
43
+ postalCode: string
44
+ country: string
45
+ isPrimary: boolean
46
+ }
47
+
48
+ export type AddressEditorField =
49
+ | 'name'
50
+ | 'purpose'
51
+ | 'companyName'
52
+ | 'addressLine1'
53
+ | 'addressLine2'
54
+ | 'buildingNumber'
55
+ | 'flatNumber'
56
+ | 'city'
57
+ | 'region'
58
+ | 'postalCode'
59
+ | 'country'
60
+ | 'isPrimary'
61
+
62
+ type AddressEditorProps<C = unknown> = {
63
+ value: AddressEditorDraft
64
+ onChange: (next: AddressEditorDraft) => void
65
+ format: AddressFormatStrategy
66
+ t: Translator
67
+ labelPrefix?: string
68
+ disabled?: boolean
69
+ errors?: Partial<Record<AddressEditorField, string>>
70
+ hidePrimaryToggle?: boolean
71
+ showFormatHint?: boolean
72
+ addressTypesAdapter?: AddressTypesAdapter<C>
73
+ addressTypesContext?: C
74
+ }
75
+
76
+ export function AddressEditor<C = unknown>({
77
+ value,
78
+ onChange,
79
+ format,
80
+ t,
81
+ labelPrefix = 'customers.people.detail.addresses',
82
+ disabled = false,
83
+ errors = {},
84
+ hidePrimaryToggle = false,
85
+ showFormatHint = true,
86
+ addressTypesAdapter,
87
+ addressTypesContext,
88
+ }: AddressEditorProps<C>) {
89
+ const label = React.useCallback(
90
+ (suffix: string, fallback?: string, params?: Record<string, string | number>) =>
91
+ t(`${labelPrefix}.${suffix}`, fallback, params),
92
+ [labelPrefix, t],
93
+ )
94
+
95
+ const [addressTypes, setAddressTypes] = React.useState<AddressTypeOption[]>([])
96
+ const [addressTypesLoading, setAddressTypesLoading] = React.useState(false)
97
+ const [addressTypeError, setAddressTypeError] = React.useState<string | null>(null)
98
+
99
+ const [typeDialogOpen, setTypeDialogOpen] = React.useState(false)
100
+ const [typeValue, setTypeValue] = React.useState('')
101
+ const [typeFormError, setTypeFormError] = React.useState<string | null>(null)
102
+ const [countryDialogOpen, setCountryDialogOpen] = React.useState(false)
103
+ const [countryQuery, setCountryQuery] = React.useState('')
104
+
105
+ const countryOptions = React.useMemo(
106
+ () =>
107
+ buildCountryOptions({
108
+ transformLabel: (code, fallback) => t(`customers.countries.${code.toLowerCase()}`, fallback ?? code),
109
+ }),
110
+ [t],
111
+ )
112
+
113
+ React.useEffect(() => {
114
+ let cancelled = false
115
+ const load = async () => {
116
+ if (!addressTypesAdapter) {
117
+ setAddressTypes([])
118
+ setAddressTypeError(null)
119
+ return
120
+ }
121
+ setAddressTypesLoading(true)
122
+ try {
123
+ const result = await addressTypesAdapter.list(addressTypesContext)
124
+ if (!cancelled) {
125
+ setAddressTypes(Array.isArray(result) ? result : [])
126
+ setAddressTypeError(null)
127
+ }
128
+ } catch (err) {
129
+ if (!cancelled) {
130
+ setAddressTypes([])
131
+ setAddressTypeError(label('types.loadError', 'Failed to load address types'))
132
+ }
133
+ } finally {
134
+ if (!cancelled) setAddressTypesLoading(false)
135
+ }
136
+ }
137
+ load().catch(() => {})
138
+ return () => {
139
+ cancelled = true
140
+ }
141
+ }, [addressTypesAdapter, addressTypesContext, label])
142
+
143
+ const current: AddressEditorDraft = {
144
+ name: value.name ?? '',
145
+ purpose: value.purpose ?? '',
146
+ companyName: value.companyName ?? '',
147
+ addressLine1: value.addressLine1 ?? '',
148
+ addressLine2: value.addressLine2 ?? '',
149
+ buildingNumber: value.buildingNumber ?? '',
150
+ flatNumber: value.flatNumber ?? '',
151
+ city: value.city ?? '',
152
+ region: value.region ?? '',
153
+ postalCode: value.postalCode ?? '',
154
+ country: value.country ?? '',
155
+ isPrimary: value.isPrimary ?? false,
156
+ }
157
+
158
+ const update = React.useCallback(
159
+ (key: keyof AddressEditorDraft, nextValue: string | boolean) => {
160
+ onChange({ ...current, [key]: nextValue })
161
+ },
162
+ [current, onChange],
163
+ )
164
+
165
+ const filteredCountryOptions = React.useMemo(() => {
166
+ const query = countryQuery.trim().toLowerCase()
167
+ if (!query.length) return countryOptions
168
+ return countryOptions.filter(
169
+ (option) => option.label.toLowerCase().includes(query) || option.code.toLowerCase().includes(query),
170
+ )
171
+ }, [countryOptions, countryQuery])
172
+
173
+ const selectedCountry = React.useMemo(() => {
174
+ const code = (current.country ?? '').toUpperCase()
175
+ if (!code.length) return null
176
+ return countryOptions.find((option) => option.code === code) ?? null
177
+ }, [countryOptions, current.country])
178
+
179
+ const handleTypeSubmit = React.useCallback(
180
+ async (event: React.FormEvent<HTMLFormElement>) => {
181
+ event.preventDefault()
182
+ const trimmed = typeValue.trim()
183
+ if (!trimmed.length) {
184
+ setTypeFormError(label('types.emptyError', 'Please provide a value'))
185
+ return
186
+ }
187
+ if (!addressTypesAdapter?.create) return
188
+ setTypeFormError(null)
189
+ const created = await addressTypesAdapter.create(trimmed, addressTypesContext)
190
+ if (created) {
191
+ setAddressTypes((prev) => {
192
+ const map = new Map(prev.map((entry) => [entry.value, entry]))
193
+ map.set(created.value, created)
194
+ return Array.from(map.values()).sort((a, b) => a.label.localeCompare(b.label))
195
+ })
196
+ }
197
+ setTypeDialogOpen(false)
198
+ setTypeValue('')
199
+ },
200
+ [addressTypesAdapter, addressTypesContext, label, typeValue],
201
+ )
202
+
203
+ const inputClass = (field: AddressEditorField) =>
204
+ [
205
+ 'w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring',
206
+ errors[field] ? 'border-red-500 focus:ring-red-500' : 'border-input bg-background',
207
+ ].join(' ')
208
+
209
+ return (
210
+ <div className="space-y-3">
211
+ <div className="grid gap-2 sm:grid-cols-2">
212
+ <Input
213
+ className={inputClass('name')}
214
+ placeholder={label('fields.label', 'Label')}
215
+ value={current.name}
216
+ onChange={(evt) => update('name', evt.target.value)}
217
+ disabled={disabled}
218
+ aria-invalid={errors.name ? 'true' : undefined}
219
+ />
220
+ <div className="flex gap-2">
221
+ <select
222
+ className={inputClass('purpose')}
223
+ value={current.purpose}
224
+ onChange={(evt) => update('purpose', evt.target.value)}
225
+ disabled={disabled}
226
+ aria-invalid={errors.purpose ? 'true' : undefined}
227
+ >
228
+ <option value="">
229
+ {addressTypesLoading
230
+ ? label('types.loading', 'Loading…')
231
+ : label('types.placeholder', 'Address type')}
232
+ </option>
233
+ {addressTypes.map((entry) => (
234
+ <option key={entry.value} value={entry.value}>
235
+ {entry.label}
236
+ </option>
237
+ ))}
238
+ </select>
239
+ {addressTypesAdapter?.create ? (
240
+ <Dialog open={typeDialogOpen} onOpenChange={setTypeDialogOpen}>
241
+ <DialogTrigger asChild>
242
+ <Button type="button" variant="outline" size="icon" className="shrink-0" disabled={disabled}>
243
+ <Plus className="h-4 w-4" />
244
+ </Button>
245
+ </DialogTrigger>
246
+ <DialogContent className="sm:max-w-md">
247
+ <DialogHeader>
248
+ <DialogTitle>{label('types.add', 'Add address type')}</DialogTitle>
249
+ <DialogDescription>
250
+ {label('types.addHint', 'Create a new address type for reuse.')}
251
+ </DialogDescription>
252
+ </DialogHeader>
253
+ <form className="space-y-3" onSubmit={handleTypeSubmit}>
254
+ <Input
255
+ autoFocus
256
+ value={typeValue}
257
+ onChange={(evt) => {
258
+ setTypeValue(evt.target.value)
259
+ if (typeFormError) setTypeFormError(null)
260
+ }}
261
+ placeholder={label('types.placeholder', 'Address type')}
262
+ disabled={disabled}
263
+ aria-invalid={typeFormError ? 'true' : undefined}
264
+ />
265
+ {typeFormError ? <p className="text-sm text-destructive">{typeFormError}</p> : null}
266
+ <DialogFooter>
267
+ <Button type="button" variant="outline" onClick={() => setTypeDialogOpen(false)} disabled={disabled}>
268
+ {label('types.cancel', 'Cancel')}
269
+ </Button>
270
+ <Button type="submit" disabled={disabled || !typeValue.trim()}>
271
+ {label('types.save', 'Save')}
272
+ </Button>
273
+ </DialogFooter>
274
+ </form>
275
+ </DialogContent>
276
+ </Dialog>
277
+ ) : null}
278
+ <Button
279
+ asChild
280
+ type="button"
281
+ variant="ghost"
282
+ size="icon"
283
+ className="shrink-0"
284
+ disabled={disabled}
285
+ title={label('types.manage', 'Manage address types')}
286
+ >
287
+ <Link
288
+ href={addressTypesAdapter?.manageHref ?? '/backend/config/dictionaries'}
289
+ aria-label={label('types.manage', 'Manage address types')}
290
+ >
291
+ <Settings className="h-4 w-4" />
292
+ </Link>
293
+ </Button>
294
+ </div>
295
+ </div>
296
+ {errors.purpose ? <p className="text-xs text-destructive">{errors.purpose}</p> : null}
297
+ {addressTypeError ? <p className="text-xs text-destructive">{addressTypeError}</p> : null}
298
+ <Input
299
+ className={inputClass('companyName')}
300
+ placeholder={label('fields.companyName', 'Company name')}
301
+ value={current.companyName}
302
+ onChange={(evt) => update('companyName', evt.target.value)}
303
+ disabled={disabled}
304
+ aria-invalid={errors.companyName ? 'true' : undefined}
305
+ />
306
+
307
+ {format === 'street_first' ? (
308
+ <div className="grid gap-2 sm:grid-cols-[1.5fr,0.7fr,0.7fr]">
309
+ <Input
310
+ className={inputClass('addressLine1')}
311
+ placeholder={label('fields.street', 'Street')}
312
+ value={current.addressLine1}
313
+ onChange={(evt) => update('addressLine1', evt.target.value)}
314
+ disabled={disabled}
315
+ aria-invalid={errors.addressLine1 ? 'true' : undefined}
316
+ />
317
+ <Input
318
+ className={inputClass('buildingNumber')}
319
+ placeholder={label('fields.buildingNumber', 'Building number')}
320
+ value={current.buildingNumber}
321
+ onChange={(evt) => update('buildingNumber', evt.target.value)}
322
+ disabled={disabled}
323
+ aria-invalid={errors.buildingNumber ? 'true' : undefined}
324
+ />
325
+ <Input
326
+ className={inputClass('flatNumber')}
327
+ placeholder={label('fields.flatNumber', 'Flat number')}
328
+ value={current.flatNumber}
329
+ onChange={(evt) => update('flatNumber', evt.target.value)}
330
+ disabled={disabled}
331
+ aria-invalid={errors.flatNumber ? 'true' : undefined}
332
+ />
333
+ </div>
334
+ ) : (
335
+ <Input
336
+ className={inputClass('addressLine1')}
337
+ placeholder={label('fields.line1', 'Address line 1')}
338
+ value={current.addressLine1}
339
+ onChange={(evt) => update('addressLine1', evt.target.value)}
340
+ disabled={disabled}
341
+ aria-invalid={errors.addressLine1 ? 'true' : undefined}
342
+ />
343
+ )}
344
+
345
+ <Input
346
+ className={inputClass('addressLine2')}
347
+ placeholder={label('fields.line2', 'Address line 2')}
348
+ value={current.addressLine2}
349
+ onChange={(evt) => update('addressLine2', evt.target.value)}
350
+ disabled={disabled}
351
+ aria-invalid={errors.addressLine2 ? 'true' : undefined}
352
+ />
353
+
354
+ {format !== 'street_first' ? (
355
+ <div className="grid gap-2 sm:grid-cols-[1.5fr,0.7fr,0.7fr]">
356
+ <Input
357
+ className={inputClass('addressLine1')}
358
+ placeholder={label('fields.street', 'Street')}
359
+ value={current.addressLine1}
360
+ onChange={(evt) => update('addressLine1', evt.target.value)}
361
+ disabled={disabled}
362
+ aria-invalid={errors.addressLine1 ? 'true' : undefined}
363
+ />
364
+ <Input
365
+ className={inputClass('buildingNumber')}
366
+ placeholder={label('fields.buildingNumber', 'Building number')}
367
+ value={current.buildingNumber}
368
+ onChange={(evt) => update('buildingNumber', evt.target.value)}
369
+ disabled={disabled}
370
+ aria-invalid={errors.buildingNumber ? 'true' : undefined}
371
+ />
372
+ <Input
373
+ className={inputClass('flatNumber')}
374
+ placeholder={label('fields.flatNumber', 'Flat number')}
375
+ value={current.flatNumber}
376
+ onChange={(evt) => update('flatNumber', evt.target.value)}
377
+ disabled={disabled}
378
+ aria-invalid={errors.flatNumber ? 'true' : undefined}
379
+ />
380
+ </div>
381
+ ) : null}
382
+
383
+ <div className="grid gap-2 sm:grid-cols-2">
384
+ <Input
385
+ className={inputClass('city')}
386
+ placeholder={label('fields.city', 'City')}
387
+ value={current.city}
388
+ onChange={(evt) => update('city', evt.target.value)}
389
+ disabled={disabled}
390
+ aria-invalid={errors.city ? 'true' : undefined}
391
+ />
392
+ <Input
393
+ className={inputClass('region')}
394
+ placeholder={label('fields.region', 'Region')}
395
+ value={current.region}
396
+ onChange={(evt) => update('region', evt.target.value)}
397
+ disabled={disabled}
398
+ aria-invalid={errors.region ? 'true' : undefined}
399
+ />
400
+ </div>
401
+ <div className="grid gap-2 sm:grid-cols-2">
402
+ <Input
403
+ className={inputClass('postalCode')}
404
+ placeholder={label('fields.postalCode', 'Postal code')}
405
+ value={current.postalCode}
406
+ onChange={(evt) => update('postalCode', evt.target.value)}
407
+ disabled={disabled}
408
+ aria-invalid={errors.postalCode ? 'true' : undefined}
409
+ />
410
+ <Dialog open={countryDialogOpen} onOpenChange={setCountryDialogOpen}>
411
+ <DialogTrigger asChild>
412
+ <button type="button" className={inputClass('country')} disabled={disabled}>
413
+ {selectedCountry?.label ?? label('fields.country', 'Country')}
414
+ </button>
415
+ </DialogTrigger>
416
+ <DialogContent className="sm:max-w-lg">
417
+ <DialogHeader>
418
+ <DialogTitle>{label('country.title', 'Choose a country')}</DialogTitle>
419
+ <DialogDescription>{label('country.subtitle', 'Search for a country')}</DialogDescription>
420
+ </DialogHeader>
421
+ <div className="space-y-3">
422
+ <Input
423
+ placeholder={label('country.search', 'Search countries')}
424
+ value={countryQuery}
425
+ onChange={(evt) => setCountryQuery(evt.target.value)}
426
+ />
427
+ <div className="max-h-64 overflow-auto rounded-md border border-border/60">
428
+ <ul className="divide-y divide-border/50">
429
+ {filteredCountryOptions.map((option) => (
430
+ <li key={option.code}>
431
+ <button
432
+ type="button"
433
+ className="flex w-full items-center justify-between px-3 py-2 text-left text-sm hover:bg-muted"
434
+ onClick={() => {
435
+ update('country', option.code)
436
+ setCountryDialogOpen(false)
437
+ }}
438
+ >
439
+ <span>{option.label}</span>
440
+ <span className="text-xs text-muted-foreground">{option.code}</span>
441
+ </button>
442
+ </li>
443
+ ))}
444
+ </ul>
445
+ </div>
446
+ </div>
447
+ </DialogContent>
448
+ </Dialog>
449
+ </div>
450
+
451
+ {showFormatHint ? (
452
+ <p className="text-xs text-muted-foreground">
453
+ {label('formatHint', 'Format based on address settings')}
454
+ </p>
455
+ ) : null}
456
+
457
+ {!hidePrimaryToggle ? (
458
+ <label className="flex items-center gap-2 text-sm text-muted-foreground">
459
+ <input
460
+ type="checkbox"
461
+ checked={current.isPrimary}
462
+ onChange={(evt) => update('isPrimary', evt.target.checked)}
463
+ disabled={disabled}
464
+ />
465
+ {label('fields.primary', 'Primary address')}
466
+ </label>
467
+ ) : null}
468
+ </div>
469
+ )
470
+ }
471
+
472
+ export default AddressEditor