@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,1284 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { ArrowUpRightSquare, Pencil, Plus, Trash2 } from 'lucide-react'
6
+ import { Button } from '@open-mercato/ui/primitives/button'
7
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
8
+ import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
+ import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
10
+ import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
11
+ import { DictionaryEntrySelect, type DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
12
+ import type { AppearanceSelectorLabels } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'
13
+ import { LoadingMessage, TabEmptyState } from './'
14
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
16
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
17
+
18
+ type Translator = (key: string, fallback?: string, params?: Record<string, string | number>) => string
19
+
20
+ export type ActivitySummary = {
21
+ id: string
22
+ activityType: string
23
+ subject?: string | null
24
+ body?: string | null
25
+ occurredAt?: string | null
26
+ createdAt: string
27
+ appearanceIcon?: string | null
28
+ appearanceColor?: string | null
29
+ entityId?: string | null
30
+ authorUserId?: string | null
31
+ authorName?: string | null
32
+ authorEmail?: string | null
33
+ dealId?: string | null
34
+ dealTitle?: string | null
35
+ customFields?: Array<{ key: string; label?: string | null; value: unknown }>
36
+ customValues?: Record<string, unknown> | null
37
+ }
38
+
39
+ export type SectionAction = {
40
+ label: React.ReactNode
41
+ onClick: () => void
42
+ disabled?: boolean
43
+ icon?: React.ReactNode
44
+ }
45
+
46
+ export type TabEmptyStateConfig = {
47
+ title: string
48
+ actionLabel: string
49
+ description?: string
50
+ }
51
+
52
+ export type ActivityCreatePayload = {
53
+ entityId: string
54
+ activityType: string
55
+ subject?: string | null
56
+ body?: string | null
57
+ occurredAt?: string | null
58
+ dealId?: string | null
59
+ customFields?: Record<string, unknown>
60
+ }
61
+
62
+ export type ActivityUpdatePayload = Partial<ActivityCreatePayload>
63
+
64
+ export type ActivitiesDataAdapter<C = unknown> = {
65
+ list: (params: { entityId: string | null; dealId?: string | null; context?: C }) => Promise<ActivitySummary[]>
66
+ create: (params: ActivityCreatePayload & { context?: C }) => Promise<void>
67
+ update: (params: { id: string; patch: ActivityUpdatePayload; context?: C }) => Promise<void>
68
+ delete: (params: { id: string; context?: C }) => Promise<void>
69
+ }
70
+
71
+ type DictionaryOption = {
72
+ value: string
73
+ label: string
74
+ color: string | null
75
+ icon: string | null
76
+ }
77
+
78
+ type ActivityTypePresentation = {
79
+ label: string
80
+ icon?: string | null
81
+ color?: string | null
82
+ }
83
+
84
+ type PendingAction =
85
+ | { kind: 'create' }
86
+ | { kind: 'update'; id: string }
87
+ | { kind: 'delete'; id: string }
88
+
89
+ const INVALID_DATE_MESSAGE = 'invalidDate'
90
+
91
+ const schema = {
92
+ validate(values: Record<string, unknown>) {
93
+ const result: { ok: boolean; errors?: Array<{ path: string; message: string }> } = { ok: true }
94
+ const activityType = typeof values.activityType === 'string' ? values.activityType.trim() : ''
95
+ if (!activityType) {
96
+ result.ok = false
97
+ result.errors = [{ path: 'activityType', message: 'required' }]
98
+ return result
99
+ }
100
+ const occurredAt = typeof values.occurredAt === 'string' ? values.occurredAt.trim() : ''
101
+ if (occurredAt.length) {
102
+ const parsed = new Date(occurredAt)
103
+ if (Number.isNaN(parsed.getTime())) {
104
+ result.ok = false
105
+ result.errors = [{ path: 'occurredAt', message: INVALID_DATE_MESSAGE }]
106
+ }
107
+ }
108
+ return result
109
+ },
110
+ }
111
+
112
+ function toLocalDateTimeInput(value?: string | null): string {
113
+ if (!value) return ''
114
+ const date = new Date(value)
115
+ if (Number.isNaN(date.getTime())) return ''
116
+ const pad = (input: number) => `${input}`.padStart(2, '0')
117
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(
118
+ date.getMinutes(),
119
+ )}`
120
+ }
121
+
122
+ function formatDateTime(value?: string | null): string | null {
123
+ if (!value) return null
124
+ const date = new Date(value)
125
+ if (Number.isNaN(date.getTime())) return null
126
+ return date.toLocaleString()
127
+ }
128
+
129
+ function formatRelativeTime(value?: string | null): string | null {
130
+ if (!value) return null
131
+ const date = new Date(value)
132
+ if (Number.isNaN(date.getTime())) return null
133
+ const now = Date.now()
134
+ const diffSeconds = (date.getTime() - now) / 1000
135
+ const absSeconds = Math.abs(diffSeconds)
136
+ const rtf =
137
+ typeof Intl !== 'undefined' && typeof Intl.RelativeTimeFormat === 'function'
138
+ ? new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' })
139
+ : null
140
+ const format = (unit: Intl.RelativeTimeFormatUnit, divisor: number) => {
141
+ const valueToFormat = Math.round(diffSeconds / divisor)
142
+ if (rtf) return rtf.format(valueToFormat, unit)
143
+ const suffix = valueToFormat <= 0 ? 'ago' : 'from now'
144
+ const magnitude = Math.abs(valueToFormat)
145
+ return `${magnitude} ${unit}${magnitude === 1 ? '' : 's'} ${suffix}`
146
+ }
147
+ if (absSeconds < 45) return format('second', 1)
148
+ if (absSeconds < 45 * 60) return format('minute', 60)
149
+ if (absSeconds < 24 * 60 * 60) return format('hour', 60 * 60)
150
+ if (absSeconds < 7 * 24 * 60 * 60) return format('day', 24 * 60 * 60)
151
+ if (absSeconds < 30 * 24 * 60 * 60) return format('week', 7 * 24 * 60 * 60)
152
+ if (absSeconds < 365 * 24 * 60 * 60) return format('month', 30 * 24 * 60 * 60)
153
+ return format('year', 365 * 24 * 60 * 60)
154
+ }
155
+
156
+ type TimelineItemHeaderProps = {
157
+ title: React.ReactNode
158
+ subtitle?: React.ReactNode
159
+ timestamp?: string | Date | null
160
+ fallbackTimestampLabel?: React.ReactNode
161
+ icon?: string | null
162
+ color?: string | null
163
+ iconSize?: 'sm' | 'md'
164
+ className?: string
165
+ renderIcon?: (icon: string, className?: string) => React.ReactNode
166
+ renderColor?: (color: string, className?: string) => React.ReactNode
167
+ }
168
+
169
+ function TimelineItemHeader({
170
+ title,
171
+ subtitle,
172
+ timestamp,
173
+ fallbackTimestampLabel,
174
+ icon,
175
+ color,
176
+ iconSize = 'md',
177
+ className,
178
+ renderIcon,
179
+ renderColor,
180
+ }: TimelineItemHeaderProps) {
181
+ const wrapperSize = iconSize === 'sm' ? 'h-6 w-6' : 'h-8 w-8'
182
+ const iconSizeClass = iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'
183
+ const resolvedTimestamp = React.useMemo(() => {
184
+ if (subtitle) return subtitle
185
+ if (!timestamp) return fallbackTimestampLabel ?? null
186
+ const value = typeof timestamp === 'string' ? timestamp : timestamp.toISOString()
187
+ const date = new Date(value)
188
+ if (Number.isNaN(date.getTime())) return fallbackTimestampLabel ?? null
189
+ const now = Date.now()
190
+ const diff = Math.abs(now - date.getTime())
191
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000
192
+ const relativeLabel = diff <= THIRTY_DAYS_MS ? formatRelativeTime(value) : null
193
+ const absoluteLabel = formatDateTime(value)
194
+ if (relativeLabel) {
195
+ return (
196
+ <span title={absoluteLabel ?? undefined}>
197
+ {relativeLabel}
198
+ </span>
199
+ )
200
+ }
201
+ return absoluteLabel ?? fallbackTimestampLabel ?? null
202
+ }, [fallbackTimestampLabel, subtitle, timestamp])
203
+
204
+ const iconNode = icon && renderIcon ? renderIcon(icon, iconSizeClass) : null
205
+
206
+ return (
207
+ <div className={['flex items-start gap-3', className].filter(Boolean).join(' ')}>
208
+ {iconNode ? (
209
+ <span className={['inline-flex items-center justify-center rounded border border-border bg-muted/40', wrapperSize].join(' ')}>
210
+ {iconNode}
211
+ </span>
212
+ ) : null}
213
+ <div className="space-y-1">
214
+ <div className="flex flex-wrap items-center gap-2">
215
+ <span className="text-sm font-semibold text-foreground">{title}</span>
216
+ {color && renderColor ? renderColor(color, 'h-3 w-3 rounded-full border border-border') : null}
217
+ </div>
218
+ {resolvedTimestamp ? <div className="text-xs text-muted-foreground">{resolvedTimestamp}</div> : null}
219
+ </div>
220
+ </div>
221
+ )
222
+ }
223
+
224
+ export type ActivityFormBaseValues = {
225
+ activityType: string
226
+ subject?: string | null
227
+ body?: string | null
228
+ occurredAt?: string | null
229
+ dealId?: string | null
230
+ }
231
+
232
+ export type ActivityFormSubmitPayload = {
233
+ base: ActivityFormBaseValues
234
+ custom: Record<string, unknown>
235
+ entityId?: string | null
236
+ }
237
+
238
+ type ActivityFormProps = {
239
+ mode: 'create' | 'edit'
240
+ initialValues?: Partial<ActivityFormBaseValues & Record<string, unknown>>
241
+ onSubmit: (payload: ActivityFormSubmitPayload) => Promise<void>
242
+ onCancel: () => void
243
+ submitLabel?: string
244
+ cancelLabel?: string
245
+ isSubmitting?: boolean
246
+ activityTypeLabels: DictionarySelectLabels
247
+ loadActivityOptions: () => Promise<DictionaryOption[]>
248
+ createActivityOption?: (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => Promise<DictionaryOption>
249
+ dealOptions?: Array<{ id: string; label: string }>
250
+ entityOptions?: Array<{ id: string; label: string }>
251
+ defaultEntityId?: string | null
252
+ manageHref?: string
253
+ customFieldEntityIds?: string[]
254
+ labelPrefix?: string
255
+ appearanceLabels?: AppearanceSelectorLabels
256
+ }
257
+
258
+ function normalizeCustomFieldSubmitValue(value: unknown): unknown {
259
+ if (Array.isArray(value)) {
260
+ return value.filter((entry) => entry !== undefined)
261
+ }
262
+ if (value === undefined) return null
263
+ return value
264
+ }
265
+
266
+ function buildActivityValidationError(errors: Array<{ path: string; message: string }>, translate: (key: string, fallback?: string) => string) {
267
+ const issue = errors[0]
268
+ if (!issue) {
269
+ throw createCrudFormError(translate('error', 'Failed to save activity.'))
270
+ }
271
+ const message = issue.message === INVALID_DATE_MESSAGE
272
+ ? translate('invalidDate', 'Invalid date')
273
+ : translate('error', 'Failed to save activity.')
274
+ const field = issue.path
275
+ throw createCrudFormError(message, field ? { [field]: message } : undefined)
276
+ }
277
+
278
+ function ActivityForm({
279
+ mode,
280
+ initialValues,
281
+ onSubmit,
282
+ onCancel,
283
+ submitLabel,
284
+ cancelLabel,
285
+ isSubmitting = false,
286
+ activityTypeLabels,
287
+ loadActivityOptions,
288
+ createActivityOption,
289
+ dealOptions,
290
+ entityOptions,
291
+ defaultEntityId,
292
+ manageHref = '/backend/config/dictionaries',
293
+ customFieldEntityIds,
294
+ labelPrefix = 'customers.people.detail.activities',
295
+ appearanceLabels,
296
+ }: ActivityFormProps) {
297
+ const tHook = useT()
298
+ const t = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])
299
+ const translate = React.useCallback(
300
+ (suffix: string, fallback?: string) => t(`${labelPrefix}.${suffix}`, fallback ?? ''),
301
+ [labelPrefix, t],
302
+ )
303
+ const [pending, setPending] = React.useState(false)
304
+
305
+ const normalizedDealOptions = React.useMemo(() => {
306
+ if (!Array.isArray(dealOptions)) return []
307
+ const seen = new Set<string>()
308
+ return dealOptions
309
+ .map((option) => {
310
+ if (!option || typeof option !== 'object') return null
311
+ const id = typeof option.id === 'string' ? option.id.trim() : ''
312
+ if (!id || seen.has(id)) return null
313
+ const label =
314
+ typeof option.label === 'string' && option.label.trim().length
315
+ ? option.label.trim()
316
+ : id
317
+ seen.add(id)
318
+ return { id, label }
319
+ })
320
+ .filter((option): option is { id: string; label: string } => !!option)
321
+ }, [dealOptions])
322
+
323
+ const normalizedEntityOptions = React.useMemo(() => {
324
+ if (!Array.isArray(entityOptions)) return []
325
+ const seen = new Set<string>()
326
+ return entityOptions
327
+ .map((option) => {
328
+ if (!option || typeof option !== 'object') return null
329
+ const id = typeof option.id === 'string' ? option.id.trim() : ''
330
+ if (!id || seen.has(id)) return null
331
+ const label =
332
+ typeof option.label === 'string' && option.label.trim().length
333
+ ? option.label.trim()
334
+ : id
335
+ seen.add(id)
336
+ return { id, label }
337
+ })
338
+ .filter((option): option is { id: string; label: string } => !!option)
339
+ }, [entityOptions])
340
+
341
+ const baseFields = React.useMemo<CrudField[]>(() => {
342
+ const fields: CrudField[] = []
343
+
344
+ if (normalizedEntityOptions.length) {
345
+ fields.push({
346
+ id: 'entityId',
347
+ label: translate('fields.entity', 'Assign to record'),
348
+ type: 'custom',
349
+ layout: 'half',
350
+ component: ({ value, setValue }) => {
351
+ const currentValue =
352
+ typeof value === 'string' && value.length ? value : normalizedEntityOptions[0]?.id ?? ''
353
+ return (
354
+ <select
355
+ className="h-9 w-full rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
356
+ value={currentValue}
357
+ onChange={(event) => setValue(event.target.value)}
358
+ >
359
+ {normalizedEntityOptions.map((option) => (
360
+ <option key={option.id} value={option.id}>
361
+ {option.label}
362
+ </option>
363
+ ))}
364
+ </select>
365
+ )
366
+ },
367
+ } as CrudField)
368
+ }
369
+
370
+ if (normalizedDealOptions.length) {
371
+ fields.push({
372
+ id: 'dealId',
373
+ label: translate('fields.deal', 'Link to deal (optional)'),
374
+ type: 'custom',
375
+ layout: 'half',
376
+ component: ({ value, setValue }) => {
377
+ const currentValue = typeof value === 'string' ? value : ''
378
+ return (
379
+ <select
380
+ className="h-9 w-full rounded border border-muted-foreground/40 bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
381
+ value={currentValue}
382
+ onChange={(event) => setValue(event.target.value)}
383
+ >
384
+ <option value="">
385
+ {translate('fields.dealPlaceholder', 'No linked deal')}
386
+ </option>
387
+ {normalizedDealOptions.map((option) => (
388
+ <option key={option.id} value={option.id}>
389
+ {option.label}
390
+ </option>
391
+ ))}
392
+ </select>
393
+ )
394
+ },
395
+ } as CrudField)
396
+ }
397
+
398
+ fields.push({
399
+ id: 'activityType',
400
+ label: translate('fields.type', 'Activity type'),
401
+ type: 'custom',
402
+ required: true,
403
+ layout: 'half',
404
+ component: ({ value, setValue }) => (
405
+ <DictionaryEntrySelect
406
+ value={typeof value === 'string' ? value : undefined}
407
+ onChange={(next) => setValue(next ?? '')}
408
+ fetchOptions={loadActivityOptions}
409
+ createOption={createActivityOption}
410
+ labels={activityTypeLabels}
411
+ allowAppearance
412
+ allowInlineCreate
413
+ appearanceLabels={appearanceLabels}
414
+ selectClassName="w-full"
415
+ manageHref={manageHref}
416
+ />
417
+ ),
418
+ } as CrudField)
419
+
420
+ fields.push({
421
+ id: 'subject',
422
+ label: translate('fields.subject', 'Subject'),
423
+ type: 'text',
424
+ layout: 'half',
425
+ placeholder: translate('subjectPlaceholder', 'Add a subject (optional)'),
426
+ } as CrudField)
427
+
428
+ fields.push({
429
+ id: 'body',
430
+ label: translate('fields.body', 'Details'),
431
+ type: 'textarea',
432
+ placeholder: translate('bodyPlaceholder', 'Describe the interaction'),
433
+ } as CrudField)
434
+
435
+ fields.push({
436
+ id: 'occurredAt',
437
+ label: translate('fields.occurredAt', 'Occurred / will occur at'),
438
+ type: 'custom',
439
+ component: ({ value, setValue }) => (
440
+ <input
441
+ type="datetime-local"
442
+ className="w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
443
+ value={typeof value === 'string' ? value : ''}
444
+ onChange={(event) => setValue(event.target.value || '')}
445
+ onFocus={(event) => {
446
+ const target = event.currentTarget as HTMLInputElement & { showPicker?: () => void }
447
+ if (typeof target.showPicker === 'function') {
448
+ try { target.showPicker() } catch { /* ignore unsupported */ }
449
+ }
450
+ }}
451
+ onClick={(event) => {
452
+ const target = event.currentTarget as HTMLInputElement & { showPicker?: () => void }
453
+ if (typeof target.showPicker === 'function') {
454
+ try { target.showPicker() } catch { /* ignore unsupported */ }
455
+ }
456
+ }}
457
+ />
458
+ ),
459
+ layout: 'half',
460
+ } as CrudField)
461
+
462
+ return fields
463
+ }, [
464
+ activityTypeLabels,
465
+ appearanceLabels,
466
+ createActivityOption,
467
+ loadActivityOptions,
468
+ manageHref,
469
+ normalizedDealOptions,
470
+ normalizedEntityOptions,
471
+ translate,
472
+ ])
473
+
474
+ const baseFieldIds = React.useMemo(() => new Set(baseFields.map((field) => field.id)), [baseFields])
475
+
476
+ const groups = React.useMemo<CrudFormGroup[]>(() => {
477
+ const detailFields: string[] = []
478
+ if (normalizedEntityOptions.length) detailFields.push('entityId')
479
+ if (normalizedDealOptions.length) detailFields.push('dealId')
480
+ detailFields.push('activityType', 'subject', 'occurredAt', 'body')
481
+ const baseGroups: CrudFormGroup[] = [
482
+ {
483
+ id: 'details',
484
+ title: translate('form.details', 'Activity details'),
485
+ column: 1,
486
+ fields: detailFields,
487
+ },
488
+ ]
489
+ baseGroups.push({
490
+ id: 'custom',
491
+ title: translate('form.customFields', 'Custom fields'),
492
+ column: 2,
493
+ kind: 'customFields',
494
+ })
495
+ return baseGroups
496
+ }, [normalizedDealOptions.length, normalizedEntityOptions.length, translate])
497
+
498
+ const handleSubmit = React.useCallback(
499
+ async (values: Record<string, unknown>) => {
500
+ if (pending || isSubmitting) return
501
+ setPending(true)
502
+ try {
503
+ const parsed = schema.validate(values)
504
+ if (!parsed.ok) {
505
+ throw buildActivityValidationError(parsed.errors ?? [], translate)
506
+ }
507
+ const rawEntityId = typeof values.entityId === 'string' ? values.entityId.trim() : ''
508
+ const resolvedEntityId = rawEntityId || (typeof defaultEntityId === 'string' ? defaultEntityId : '')
509
+ const rawDealId = typeof values.dealId === 'string' ? values.dealId.trim() : ''
510
+ const base: ActivityFormBaseValues = {
511
+ activityType: typeof values.activityType === 'string' ? values.activityType.trim() : '',
512
+ subject: typeof values.subject === 'string' && values.subject.trim().length ? values.subject.trim() : undefined,
513
+ body: typeof values.body === 'string' && values.body.trim().length ? values.body.trim() : undefined,
514
+ occurredAt: typeof values.occurredAt === 'string' && values.occurredAt.trim().length
515
+ ? new Date(values.occurredAt as string).toISOString()
516
+ : undefined,
517
+ dealId: rawDealId.length ? rawDealId : undefined,
518
+ }
519
+ const reservedCustomKeys = new Set(['entityId', 'dealId'])
520
+ const customEntries = collectCustomFieldValues(values, {
521
+ transform: (value) => normalizeCustomFieldSubmitValue(value),
522
+ accept: (fieldId) => !reservedCustomKeys.has(fieldId),
523
+ })
524
+ Object.entries(values).forEach(([key, value]) => {
525
+ if (key.startsWith('cf_')) return
526
+ if (!baseFieldIds.has(key) && key !== 'id') {
527
+ if (reservedCustomKeys.has(key)) return
528
+ customEntries[key] = normalizeCustomFieldSubmitValue(value)
529
+ }
530
+ })
531
+ await onSubmit({ base, custom: customEntries, entityId: resolvedEntityId.length ? resolvedEntityId : undefined })
532
+ } finally {
533
+ setPending(false)
534
+ }
535
+ },
536
+ [baseFieldIds, defaultEntityId, isSubmitting, onSubmit, pending, translate],
537
+ )
538
+
539
+ const embeddedInitialValues = React.useMemo(() => {
540
+ const occurredAt = toLocalDateTimeInput(initialValues?.occurredAt ?? null)
541
+ const resolvedEntity = (() => {
542
+ const raw = typeof (initialValues as Record<string, unknown> | undefined)?.entityId === 'string'
543
+ ? (initialValues as Record<string, unknown>).entityId as string
544
+ : typeof defaultEntityId === 'string'
545
+ ? defaultEntityId
546
+ : normalizedEntityOptions[0]?.id ?? ''
547
+ return raw ?? ''
548
+ })()
549
+ const resolvedDeal = typeof (initialValues as Record<string, unknown> | undefined)?.dealId === 'string'
550
+ ? (initialValues as Record<string, unknown>).dealId as string
551
+ : ''
552
+
553
+ return {
554
+ entityId: resolvedEntity,
555
+ dealId: resolvedDeal,
556
+ activityType: initialValues?.activityType ?? '',
557
+ subject: initialValues?.subject ?? '',
558
+ body: initialValues?.body ?? '',
559
+ occurredAt,
560
+ ...Object.fromEntries(
561
+ Object.entries(initialValues ?? {})
562
+ .filter(([key]) => {
563
+ if (!key.startsWith('cf_')) return false
564
+ const trimmed = key.slice(3)
565
+ return trimmed !== 'entityId' && trimmed !== 'dealId'
566
+ })
567
+ .map(([key, value]) => [key, value]),
568
+ ),
569
+ }
570
+ }, [defaultEntityId, initialValues, normalizedEntityOptions])
571
+
572
+ return (
573
+ <CrudForm<Record<string, unknown>>
574
+ embedded
575
+ fields={baseFields}
576
+ groups={groups}
577
+ initialValues={embeddedInitialValues}
578
+ onSubmit={handleSubmit}
579
+ submitLabel={submitLabel ?? (mode === 'edit'
580
+ ? translate('update', 'Update activity (⌘/Ctrl + Enter)')
581
+ : translate('save', 'Save activity (⌘/Ctrl + Enter)'))}
582
+ extraActions={(
583
+ <Button
584
+ type="button"
585
+ variant="outline"
586
+ onClick={onCancel}
587
+ disabled={pending || isSubmitting}
588
+ >
589
+ {cancelLabel ?? translate('cancel', 'Cancel')}
590
+ </Button>
591
+ )}
592
+ entityIds={customFieldEntityIds}
593
+ />
594
+ )
595
+ }
596
+
597
+ type ActivityDialogProps = {
598
+ open: boolean
599
+ mode: 'create' | 'edit'
600
+ onOpenChange: (next: boolean) => void
601
+ initialValues?: Partial<ActivityFormBaseValues & Record<string, unknown>>
602
+ onSubmit: (payload: ActivityFormSubmitPayload) => Promise<void>
603
+ isSubmitting?: boolean
604
+ activityTypeLabels: DictionarySelectLabels
605
+ loadActivityOptions: () => Promise<DictionaryOption[]>
606
+ createActivityOption?: (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => Promise<DictionaryOption>
607
+ titles?: {
608
+ create?: string
609
+ edit?: string
610
+ }
611
+ submitLabels?: {
612
+ create?: string
613
+ edit?: string
614
+ }
615
+ cancelLabel?: string
616
+ dealOptions?: Array<{ id: string; label: string }>
617
+ entityOptions?: Array<{ id: string; label: string }>
618
+ defaultEntityId?: string | null
619
+ manageHref?: string
620
+ customFieldEntityIds?: string[]
621
+ labelPrefix?: string
622
+ appearanceLabels?: AppearanceSelectorLabels
623
+ }
624
+
625
+ function ActivityDialog({
626
+ open,
627
+ mode,
628
+ onOpenChange,
629
+ initialValues,
630
+ onSubmit,
631
+ isSubmitting,
632
+ activityTypeLabels,
633
+ loadActivityOptions,
634
+ createActivityOption,
635
+ titles,
636
+ submitLabels,
637
+ cancelLabel,
638
+ dealOptions,
639
+ entityOptions,
640
+ defaultEntityId,
641
+ manageHref,
642
+ customFieldEntityIds,
643
+ labelPrefix = 'customers.people.detail.activities',
644
+ appearanceLabels,
645
+ }: ActivityDialogProps) {
646
+ const tHook = useT()
647
+ const t = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])
648
+ const translate = React.useCallback(
649
+ (suffix: string, fallback?: string, params?: Record<string, string | number>) =>
650
+ t(`${labelPrefix}.${suffix}`, fallback ?? '', params),
651
+ [labelPrefix, t],
652
+ )
653
+
654
+ const dialogTitle =
655
+ mode === 'edit'
656
+ ? titles?.edit ?? translate('editTitle', 'Edit activity')
657
+ : titles?.create ?? translate('addTitle', 'Add activity')
658
+
659
+ const resolvedSubmitLabel =
660
+ mode === 'edit'
661
+ ? submitLabels?.edit ?? translate('update', 'Update activity (⌘/Ctrl + Enter)')
662
+ : submitLabels?.create ?? translate('save', 'Save activity (⌘/Ctrl + Enter)')
663
+
664
+ const resolvedCancelLabel = cancelLabel ?? translate('cancel', 'Cancel')
665
+
666
+ const handleCancel = React.useCallback(() => {
667
+ onOpenChange(false)
668
+ }, [onOpenChange])
669
+
670
+ return (
671
+ <Dialog open={open} onOpenChange={onOpenChange}>
672
+ <DialogContent className="sm:max-w-3xl">
673
+ <DialogHeader>
674
+ <DialogTitle>{dialogTitle}</DialogTitle>
675
+ </DialogHeader>
676
+ <ActivityForm
677
+ mode={mode}
678
+ initialValues={initialValues}
679
+ onSubmit={onSubmit}
680
+ onCancel={handleCancel}
681
+ submitLabel={resolvedSubmitLabel}
682
+ cancelLabel={resolvedCancelLabel}
683
+ isSubmitting={isSubmitting}
684
+ activityTypeLabels={activityTypeLabels}
685
+ loadActivityOptions={loadActivityOptions}
686
+ createActivityOption={createActivityOption}
687
+ dealOptions={dealOptions}
688
+ entityOptions={entityOptions}
689
+ defaultEntityId={defaultEntityId}
690
+ manageHref={manageHref}
691
+ customFieldEntityIds={customFieldEntityIds}
692
+ labelPrefix={labelPrefix}
693
+ appearanceLabels={appearanceLabels}
694
+ />
695
+ </DialogContent>
696
+ </Dialog>
697
+ )
698
+ }
699
+
700
+ export type ActivitiesSectionProps<C = unknown> = {
701
+ entityId: string | null
702
+ dealId?: string | null
703
+ addActionLabel: string
704
+ emptyState: TabEmptyStateConfig
705
+ onActionChange?: (action: SectionAction | null) => void
706
+ onLoadingChange?: (isLoading: boolean) => void
707
+ dealOptions?: Array<{ id: string; label: string }>
708
+ entityOptions?: Array<{ id: string; label: string }>
709
+ defaultEntityId?: string | null
710
+ dataAdapter: ActivitiesDataAdapter<C>
711
+ dataContext?: C
712
+ activityTypeLabels: DictionarySelectLabels
713
+ loadActivityOptions: () => Promise<DictionaryOption[]>
714
+ createActivityOption?: (input: { value: string; label?: string; color?: string | null; icon?: string | null }) => Promise<DictionaryOption>
715
+ resolveActivityPresentation?: (activity: ActivitySummary) => ActivityTypePresentation
716
+ renderCustomFields?: (activity: ActivitySummary) => React.ReactNode
717
+ customFieldEntityIds?: string[]
718
+ labelPrefix?: string
719
+ renderIcon?: (icon: string, className?: string) => React.ReactNode
720
+ renderColor?: (color: string, className?: string) => React.ReactNode
721
+ appearanceLabels?: AppearanceSelectorLabels
722
+ dealLinkHref?: (dealId: string) => string
723
+ manageHref?: string
724
+ }
725
+
726
+ export function ActivitiesSection<C = unknown>({
727
+ entityId,
728
+ dealId,
729
+ addActionLabel,
730
+ emptyState,
731
+ onActionChange,
732
+ onLoadingChange,
733
+ dealOptions,
734
+ entityOptions,
735
+ defaultEntityId,
736
+ dataAdapter,
737
+ dataContext,
738
+ activityTypeLabels,
739
+ loadActivityOptions,
740
+ createActivityOption,
741
+ resolveActivityPresentation,
742
+ renderCustomFields,
743
+ customFieldEntityIds,
744
+ labelPrefix = 'customers.people.detail.activities',
745
+ renderIcon,
746
+ renderColor,
747
+ appearanceLabels,
748
+ dealLinkHref,
749
+ manageHref,
750
+ }: ActivitiesSectionProps<C>) {
751
+ const tHook = useT()
752
+ const baseTranslator = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])
753
+ const translate = React.useCallback(
754
+ (suffix: string, fallback?: string, params?: Record<string, string | number>) =>
755
+ baseTranslator(`${labelPrefix}.${suffix}`, fallback ?? '', params),
756
+ [baseTranslator, labelPrefix],
757
+ )
758
+ const resolvedDefaultEntityId = React.useMemo(() => {
759
+ const primary = typeof entityId === 'string' ? entityId.trim() : ''
760
+ if (primary.length) return primary
761
+ const fallback = typeof defaultEntityId === 'string' ? defaultEntityId.trim() : ''
762
+ if (fallback.length) return fallback
763
+ if (Array.isArray(entityOptions)) {
764
+ for (const option of entityOptions) {
765
+ if (!option || typeof option !== 'object') continue
766
+ const id = typeof option.id === 'string' ? option.id.trim() : ''
767
+ if (id.length) return id
768
+ }
769
+ }
770
+ return ''
771
+ }, [defaultEntityId, entityId, entityOptions])
772
+
773
+ const resolveEntityForSubmission = React.useCallback(
774
+ (input?: string | null) => {
775
+ const candidate = typeof input === 'string' ? input.trim() : ''
776
+ if (candidate.length) return candidate
777
+ return resolvedDefaultEntityId.length ? resolvedDefaultEntityId : null
778
+ },
779
+ [resolvedDefaultEntityId],
780
+ )
781
+
782
+ const [activities, setActivities] = React.useState<ActivitySummary[]>([])
783
+ const [isLoading, setIsLoading] = React.useState<boolean>(() => {
784
+ const entity = typeof entityId === 'string' ? entityId.trim() : ''
785
+ const deal = typeof dealId === 'string' ? dealId.trim() : ''
786
+ return Boolean(entity || deal || resolvedDefaultEntityId)
787
+ })
788
+ const [loadError, setLoadError] = React.useState<string | null>(null)
789
+ const [pendingAction, setPendingAction] = React.useState<PendingAction | null>(null)
790
+ const [dialogOpen, setDialogOpen] = React.useState(false)
791
+ const [dialogMode, setDialogMode] = React.useState<'create' | 'edit'>('create')
792
+ const [editingActivityId, setEditingActivityId] = React.useState<string | null>(null)
793
+ const [initialValues, setInitialValues] = React.useState<Partial<ActivityFormBaseValues & Record<string, unknown>> | undefined>(undefined)
794
+ const [visibleCount, setVisibleCount] = React.useState(0)
795
+ const pendingCounterRef = React.useRef(0)
796
+
797
+ const t = translate
798
+
799
+ const pushLoading = React.useCallback(() => {
800
+ pendingCounterRef.current += 1
801
+ if (pendingCounterRef.current === 1) {
802
+ onLoadingChange?.(true)
803
+ }
804
+ }, [onLoadingChange])
805
+
806
+ const popLoading = React.useCallback(() => {
807
+ pendingCounterRef.current = Math.max(0, pendingCounterRef.current - 1)
808
+ if (pendingCounterRef.current === 0) {
809
+ onLoadingChange?.(false)
810
+ }
811
+ }, [onLoadingChange])
812
+
813
+ const updateVisibleCount = React.useCallback((length: number) => {
814
+ if (!length) {
815
+ setVisibleCount(0)
816
+ return
817
+ }
818
+ const baseline = Math.min(5, length)
819
+ setVisibleCount((prev) => {
820
+ if (prev >= length) {
821
+ return Math.min(prev, length)
822
+ }
823
+ return Math.min(Math.max(prev, baseline), length)
824
+ })
825
+ }, [])
826
+
827
+ const loadActivities = React.useCallback(async () => {
828
+ const queryEntityId = typeof entityId === 'string' ? entityId.trim() : ''
829
+ const queryDealId = typeof dealId === 'string' ? dealId.trim() : ''
830
+ if (!queryEntityId && !queryDealId) {
831
+ setActivities([])
832
+ setLoadError(null)
833
+ updateVisibleCount(0)
834
+ return
835
+ }
836
+ pushLoading()
837
+ setIsLoading(true)
838
+ try {
839
+ const items = await dataAdapter.list({
840
+ entityId: queryEntityId || null,
841
+ dealId: queryDealId || null,
842
+ context: dataContext,
843
+ })
844
+ setActivities(items)
845
+ setLoadError(null)
846
+ updateVisibleCount(items.length)
847
+ } catch (err) {
848
+ const message =
849
+ err instanceof Error
850
+ ? err.message
851
+ : t('loadError', 'Failed to load activities.')
852
+ setLoadError(message)
853
+ } finally {
854
+ setIsLoading(false)
855
+ popLoading()
856
+ }
857
+ }, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading, t, updateVisibleCount])
858
+
859
+ React.useEffect(() => {
860
+ updateVisibleCount(activities.length)
861
+ }, [activities.length, updateVisibleCount])
862
+
863
+ React.useEffect(() => {
864
+ const queryEntityId = typeof entityId === 'string' ? entityId.trim() : ''
865
+ const queryDealId = typeof dealId === 'string' ? dealId.trim() : ''
866
+ if (!queryEntityId && !queryDealId) {
867
+ setActivities([])
868
+ setLoadError(null)
869
+ setIsLoading(false)
870
+ pendingCounterRef.current = 0
871
+ onLoadingChange?.(false)
872
+ updateVisibleCount(0)
873
+ return
874
+ }
875
+ loadActivities().catch(() => {})
876
+ }, [dealId, entityId, loadActivities, onLoadingChange, updateVisibleCount])
877
+
878
+ const openCreateDialog = React.useCallback(() => {
879
+ setDialogMode('create')
880
+ setEditingActivityId(null)
881
+ setInitialValues(undefined)
882
+ setDialogOpen(true)
883
+ }, [])
884
+
885
+ const openEditDialog = React.useCallback((activity: ActivitySummary) => {
886
+ setDialogMode('edit')
887
+ setEditingActivityId(activity.id)
888
+ const baseValues: Partial<ActivityFormBaseValues & Record<string, unknown>> = {
889
+ activityType: activity.activityType,
890
+ subject: activity.subject ?? '',
891
+ body: activity.body ?? '',
892
+ occurredAt: activity.occurredAt ?? activity.createdAt ?? null,
893
+ dealId: activity.dealId ?? '',
894
+ entityId: activity.entityId ?? '',
895
+ }
896
+ const customEntries = Array.isArray(activity.customFields) ? activity.customFields : []
897
+ customEntries.forEach((entry) => {
898
+ if (entry.key === 'entityId' || entry.key === 'dealId') return
899
+ baseValues[`cf_${entry.key}`] = entry.value ?? null
900
+ })
901
+ setInitialValues(baseValues)
902
+ setDialogOpen(true)
903
+ }, [])
904
+
905
+ const closeDialog = React.useCallback(() => {
906
+ setDialogOpen(false)
907
+ setDialogMode('create')
908
+ setEditingActivityId(null)
909
+ setInitialValues(undefined)
910
+ }, [])
911
+
912
+ const handleDialogOpenChange = React.useCallback(
913
+ (next: boolean) => {
914
+ if (!next) {
915
+ closeDialog()
916
+ } else {
917
+ setDialogOpen(true)
918
+ }
919
+ },
920
+ [closeDialog],
921
+ )
922
+
923
+ const handleCreate = React.useCallback(
924
+ async ({ base, custom, entityId: formEntityId }: ActivityFormSubmitPayload) => {
925
+ const submissionEntityId = resolveEntityForSubmission(formEntityId)
926
+ if (!submissionEntityId) {
927
+ const message = t('entityMissing', 'Select a related record before saving.')
928
+ flash(message, 'error')
929
+ throw new Error(message)
930
+ }
931
+ setPendingAction({ kind: 'create' })
932
+ pushLoading()
933
+ try {
934
+ const payload: ActivityCreatePayload = {
935
+ entityId: submissionEntityId,
936
+ activityType: base.activityType,
937
+ subject: base.subject ?? undefined,
938
+ body: base.body ?? undefined,
939
+ occurredAt: base.occurredAt ?? undefined,
940
+ dealId: base.dealId ?? undefined,
941
+ customFields: Object.keys(custom).length ? custom : undefined,
942
+ }
943
+ await dataAdapter.create({ ...payload, context: dataContext })
944
+ await loadActivities()
945
+ flash(t('success', 'Activity saved'), 'success')
946
+ } catch (err) {
947
+ const message =
948
+ err instanceof Error
949
+ ? err.message
950
+ : t('error', 'Failed to save activity')
951
+ throw err instanceof Error ? err : new Error(message)
952
+ } finally {
953
+ setPendingAction(null)
954
+ popLoading()
955
+ }
956
+ },
957
+ [dataAdapter, dataContext, loadActivities, popLoading, pushLoading, resolveEntityForSubmission, t],
958
+ )
959
+
960
+ const handleUpdate = React.useCallback(
961
+ async (activityId: string, { base, custom, entityId: formEntityId }: ActivityFormSubmitPayload) => {
962
+ const submissionEntityId = resolveEntityForSubmission(formEntityId)
963
+ if (!submissionEntityId) {
964
+ const message = t('entityMissing', 'Select a related record before saving.')
965
+ flash(message, 'error')
966
+ throw new Error(message)
967
+ }
968
+ setPendingAction({ kind: 'update', id: activityId })
969
+ pushLoading()
970
+ try {
971
+ const patch: ActivityUpdatePayload = {
972
+ entityId: submissionEntityId,
973
+ activityType: base.activityType,
974
+ subject: base.subject ?? undefined,
975
+ body: base.body ?? undefined,
976
+ occurredAt: base.occurredAt ?? undefined,
977
+ dealId: base.dealId ?? undefined,
978
+ customFields: Object.keys(custom).length ? custom : undefined,
979
+ }
980
+ await dataAdapter.update({ id: activityId, patch, context: dataContext })
981
+ await loadActivities()
982
+ flash(t('updateSuccess', 'Activity updated.'), 'success')
983
+ } catch (err) {
984
+ const message =
985
+ err instanceof Error
986
+ ? err.message
987
+ : t('error', 'Failed to save activity')
988
+ throw err instanceof Error ? err : new Error(message)
989
+ } finally {
990
+ setPendingAction(null)
991
+ popLoading()
992
+ }
993
+ },
994
+ [dataAdapter, dataContext, loadActivities, popLoading, pushLoading, resolveEntityForSubmission, t],
995
+ )
996
+
997
+ const handleDelete = React.useCallback(
998
+ async (activity: ActivitySummary) => {
999
+ if (!activity.id) return
1000
+ const confirmed =
1001
+ typeof window === 'undefined'
1002
+ ? true
1003
+ : window.confirm(
1004
+ t(
1005
+ 'deleteConfirm',
1006
+ 'Delete this activity? This action cannot be undone.',
1007
+ ),
1008
+ )
1009
+ if (!confirmed) return
1010
+ setPendingAction({ kind: 'delete', id: activity.id })
1011
+ try {
1012
+ await dataAdapter.delete({ id: activity.id, context: dataContext })
1013
+ setActivities((prev) => prev.filter((existing) => existing.id !== activity.id))
1014
+ flash(t('deleteSuccess', 'Activity deleted.'), 'success')
1015
+ } catch (err) {
1016
+ const message =
1017
+ err instanceof Error
1018
+ ? err.message
1019
+ : t('deleteError', 'Failed to delete activity.')
1020
+ flash(message, 'error')
1021
+ throw err instanceof Error ? err : new Error(message)
1022
+ } finally {
1023
+ setPendingAction(null)
1024
+ }
1025
+ },
1026
+ [dataAdapter, dataContext, t],
1027
+ )
1028
+
1029
+ const handleDialogSubmit = React.useCallback(
1030
+ async (payload: ActivityFormSubmitPayload) => {
1031
+ if (dialogMode === 'edit' && editingActivityId) {
1032
+ await handleUpdate(editingActivityId, payload)
1033
+ } else {
1034
+ await handleCreate(payload)
1035
+ }
1036
+ closeDialog()
1037
+ },
1038
+ [closeDialog, dialogMode, editingActivityId, handleCreate, handleUpdate],
1039
+ )
1040
+
1041
+ React.useEffect(() => {
1042
+ if (!onActionChange) return
1043
+ if (activities.length === 0) {
1044
+ onActionChange(null)
1045
+ return () => {
1046
+ onActionChange(null)
1047
+ }
1048
+ }
1049
+ const disabled = resolveEntityForSubmission(null) === null || pendingAction !== null || isLoading
1050
+ const action: SectionAction = {
1051
+ label: (
1052
+ <span className="inline-flex items-center gap-1.5">
1053
+ <Plus className="h-4 w-4" />
1054
+ {addActionLabel}
1055
+ </span>
1056
+ ),
1057
+ onClick: () => {
1058
+ if (!disabled) openCreateDialog()
1059
+ },
1060
+ disabled,
1061
+ }
1062
+ onActionChange(action)
1063
+ return () => {
1064
+ onActionChange(null)
1065
+ }
1066
+ }, [
1067
+ activities.length,
1068
+ addActionLabel,
1069
+ isLoading,
1070
+ onActionChange,
1071
+ openCreateDialog,
1072
+ pendingAction,
1073
+ resolveEntityForSubmission,
1074
+ ])
1075
+
1076
+ const isFormPending =
1077
+ pendingAction?.kind === 'create' ||
1078
+ (pendingAction?.kind === 'update' && pendingAction.id === editingActivityId)
1079
+ const visibleActivities = React.useMemo(
1080
+ () => activities.slice(0, visibleCount),
1081
+ [activities, visibleCount],
1082
+ )
1083
+ const hasMoreActivities = visibleCount < activities.length
1084
+ const loadMoreLabel = t('loadMore', 'Load more activities')
1085
+
1086
+ const handleLoadMore = React.useCallback(() => {
1087
+ setVisibleCount((prev) => {
1088
+ if (prev >= activities.length) return prev
1089
+ return Math.min(prev + 5, activities.length)
1090
+ })
1091
+ }, [activities.length])
1092
+
1093
+ const resolvePresentation = React.useCallback(
1094
+ (activity: ActivitySummary): ActivityTypePresentation => {
1095
+ if (resolveActivityPresentation) return resolveActivityPresentation(activity)
1096
+ return {
1097
+ label: activity.activityType,
1098
+ icon: activity.appearanceIcon ?? null,
1099
+ color: activity.appearanceColor ?? null,
1100
+ }
1101
+ },
1102
+ [resolveActivityPresentation],
1103
+ )
1104
+
1105
+ const resolveDealHref = React.useCallback(
1106
+ (id: string) => (dealLinkHref ? dealLinkHref(id) : `/backend/customers/deals/${encodeURIComponent(id)}`),
1107
+ [dealLinkHref],
1108
+ )
1109
+
1110
+ return (
1111
+ <div className="mt-3 space-y-4">
1112
+ {loadError ? (
1113
+ <div className="rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive">
1114
+ {loadError}
1115
+ </div>
1116
+ ) : null}
1117
+ <div className="space-y-4">
1118
+ {isLoading && activities.length === 0 ? (
1119
+ <LoadingMessage
1120
+ label={t('loading', 'Loading activities…')}
1121
+ className="border-0 bg-transparent p-0 py-8 justify-center"
1122
+ />
1123
+ ) : (
1124
+ <>
1125
+ {!isLoading && activities.length === 0 && !dialogOpen ? (
1126
+ <TabEmptyState
1127
+ title={emptyState.title}
1128
+ action={{
1129
+ label: emptyState.actionLabel,
1130
+ onClick: openCreateDialog,
1131
+ disabled: resolveEntityForSubmission(null) === null || pendingAction !== null,
1132
+ }}
1133
+ />
1134
+ ) : null}
1135
+ {visibleActivities.length > 0
1136
+ ? visibleActivities.map((activity) => {
1137
+ const presentation = resolvePresentation(activity)
1138
+ const timestampValue = activity.occurredAt ?? activity.createdAt ?? null
1139
+ const occurredLabel =
1140
+ formatDateTime(timestampValue) ?? t('noDate', 'No date provided')
1141
+ const authorLabel = activity.authorName ?? activity.authorEmail ?? null
1142
+ const loggedByText = authorLabel
1143
+ ? (() => {
1144
+ const translated = t('loggedBy', `Logged by ${authorLabel}`, { user: authorLabel })
1145
+ if (
1146
+ !translated ||
1147
+ translated.includes('{{') ||
1148
+ translated.includes('{user')
1149
+ ) {
1150
+ return `Logged by ${authorLabel}`
1151
+ }
1152
+ return translated
1153
+ })()
1154
+ : null
1155
+ const isUpdatePending = pendingAction?.kind === 'update' && pendingAction.id === activity.id
1156
+ const isDeletePending = pendingAction?.kind === 'delete' && pendingAction.id === activity.id
1157
+
1158
+ return (
1159
+ <div
1160
+ key={activity.id}
1161
+ className="group space-y-3 rounded-lg border bg-card p-4 transition hover:border-border/80 cursor-pointer"
1162
+ role="button"
1163
+ tabIndex={0}
1164
+ onClick={() => openEditDialog(activity)}
1165
+ onKeyDown={(event) => {
1166
+ if (event.key === 'Enter' || event.key === ' ') {
1167
+ event.preventDefault()
1168
+ openEditDialog(activity)
1169
+ }
1170
+ }}
1171
+ >
1172
+ <div className="flex flex-wrap items-start justify-between gap-3">
1173
+ <div className="space-y-1">
1174
+ <TimelineItemHeader
1175
+ title={presentation.label}
1176
+ timestamp={timestampValue}
1177
+ fallbackTimestampLabel={occurredLabel}
1178
+ icon={presentation.icon}
1179
+ color={presentation.color}
1180
+ renderIcon={renderIcon}
1181
+ renderColor={renderColor}
1182
+ />
1183
+ {activity.dealId ? (
1184
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
1185
+ <ArrowUpRightSquare className="h-3.5 w-3.5" />
1186
+ <Link
1187
+ href={resolveDealHref(activity.dealId)}
1188
+ className="font-medium text-foreground hover:underline"
1189
+ onClick={(event) => event.stopPropagation()}
1190
+ >
1191
+ {activity.dealTitle && activity.dealTitle.length
1192
+ ? activity.dealTitle
1193
+ : t('linkedDeal', 'Linked deal')}
1194
+ </Link>
1195
+ </div>
1196
+ ) : null}
1197
+ </div>
1198
+ <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
1199
+ <Button
1200
+ type="button"
1201
+ variant="ghost"
1202
+ size="icon"
1203
+ onClick={(event) => {
1204
+ event.stopPropagation()
1205
+ openEditDialog(activity)
1206
+ }}
1207
+ disabled={pendingAction !== null}
1208
+ >
1209
+ {isUpdatePending ? (
1210
+ <span className="relative flex h-4 w-4 items-center justify-center">
1211
+ <span className="absolute h-4 w-4 animate-spin rounded-full border border-primary border-t-transparent" />
1212
+ </span>
1213
+ ) : (
1214
+ <Pencil className="h-4 w-4" />
1215
+ )}
1216
+ </Button>
1217
+ <Button
1218
+ type="button"
1219
+ variant="ghost"
1220
+ size="icon"
1221
+ onClick={(event) => {
1222
+ event.stopPropagation()
1223
+ handleDelete(activity).catch(() => {})
1224
+ }}
1225
+ disabled={pendingAction !== null}
1226
+ >
1227
+ {isDeletePending ? (
1228
+ <span className="relative flex h-4 w-4 items-center justify-center text-destructive">
1229
+ <span className="absolute h-4 w-4 animate-spin rounded-full border border-destructive border-t-transparent" />
1230
+ </span>
1231
+ ) : (
1232
+ <Trash2 className="h-4 w-4" />
1233
+ )}
1234
+ </Button>
1235
+ </div>
1236
+ </div>
1237
+ {activity.subject ? <p className="text-sm font-medium">{activity.subject}</p> : null}
1238
+ {activity.body ? (
1239
+ <p className="text-sm whitespace-pre-wrap text-muted-foreground">{activity.body}</p>
1240
+ ) : null}
1241
+ {renderCustomFields ? renderCustomFields(activity) : null}
1242
+ {loggedByText ? (
1243
+ <p className="text-xs text-muted-foreground">{loggedByText}</p>
1244
+ ) : null}
1245
+ </div>
1246
+ )
1247
+ })
1248
+ : null}
1249
+ {hasMoreActivities ? (
1250
+ <div className="flex justify-center">
1251
+ <Button variant="outline" size="sm" onClick={handleLoadMore} disabled={pendingAction !== null}>
1252
+ {loadMoreLabel}
1253
+ </Button>
1254
+ </div>
1255
+ ) : null}
1256
+ </>
1257
+ )}
1258
+ </div>
1259
+
1260
+ <ActivityDialog
1261
+ open={dialogOpen}
1262
+ mode={dialogMode}
1263
+ onOpenChange={handleDialogOpenChange}
1264
+ initialValues={initialValues}
1265
+ onSubmit={async (payload) => {
1266
+ await handleDialogSubmit(payload)
1267
+ }}
1268
+ isSubmitting={Boolean(isFormPending)}
1269
+ activityTypeLabels={activityTypeLabels}
1270
+ loadActivityOptions={loadActivityOptions}
1271
+ createActivityOption={createActivityOption}
1272
+ dealOptions={dealOptions}
1273
+ entityOptions={entityOptions}
1274
+ defaultEntityId={resolvedDefaultEntityId || undefined}
1275
+ manageHref={manageHref}
1276
+ customFieldEntityIds={customFieldEntityIds}
1277
+ labelPrefix={labelPrefix}
1278
+ appearanceLabels={appearanceLabels}
1279
+ />
1280
+ </div>
1281
+ )
1282
+ }
1283
+
1284
+ export default ActivitiesSection