@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,962 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
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 } from "@open-mercato/ui/backend/CrudForm";
10
+ import { collectCustomFieldValues } from "@open-mercato/ui/backend/utils/customFieldValues";
11
+ import { DictionaryEntrySelect } from "@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect";
12
+ import { LoadingMessage, TabEmptyState } from ".//index.js";
13
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
14
+ import { createTranslatorWithFallback } from "@open-mercato/shared/lib/i18n/translate";
15
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@open-mercato/ui/primitives/dialog";
16
+ const INVALID_DATE_MESSAGE = "invalidDate";
17
+ const schema = {
18
+ validate(values) {
19
+ const result = { ok: true };
20
+ const activityType = typeof values.activityType === "string" ? values.activityType.trim() : "";
21
+ if (!activityType) {
22
+ result.ok = false;
23
+ result.errors = [{ path: "activityType", message: "required" }];
24
+ return result;
25
+ }
26
+ const occurredAt = typeof values.occurredAt === "string" ? values.occurredAt.trim() : "";
27
+ if (occurredAt.length) {
28
+ const parsed = new Date(occurredAt);
29
+ if (Number.isNaN(parsed.getTime())) {
30
+ result.ok = false;
31
+ result.errors = [{ path: "occurredAt", message: INVALID_DATE_MESSAGE }];
32
+ }
33
+ }
34
+ return result;
35
+ }
36
+ };
37
+ function toLocalDateTimeInput(value) {
38
+ if (!value) return "";
39
+ const date = new Date(value);
40
+ if (Number.isNaN(date.getTime())) return "";
41
+ const pad = (input) => `${input}`.padStart(2, "0");
42
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(
43
+ date.getMinutes()
44
+ )}`;
45
+ }
46
+ function formatDateTime(value) {
47
+ if (!value) return null;
48
+ const date = new Date(value);
49
+ if (Number.isNaN(date.getTime())) return null;
50
+ return date.toLocaleString();
51
+ }
52
+ function formatRelativeTime(value) {
53
+ if (!value) return null;
54
+ const date = new Date(value);
55
+ if (Number.isNaN(date.getTime())) return null;
56
+ const now = Date.now();
57
+ const diffSeconds = (date.getTime() - now) / 1e3;
58
+ const absSeconds = Math.abs(diffSeconds);
59
+ const rtf = typeof Intl !== "undefined" && typeof Intl.RelativeTimeFormat === "function" ? new Intl.RelativeTimeFormat(void 0, { numeric: "auto" }) : null;
60
+ const format = (unit, divisor) => {
61
+ const valueToFormat = Math.round(diffSeconds / divisor);
62
+ if (rtf) return rtf.format(valueToFormat, unit);
63
+ const suffix = valueToFormat <= 0 ? "ago" : "from now";
64
+ const magnitude = Math.abs(valueToFormat);
65
+ return `${magnitude} ${unit}${magnitude === 1 ? "" : "s"} ${suffix}`;
66
+ };
67
+ if (absSeconds < 45) return format("second", 1);
68
+ if (absSeconds < 45 * 60) return format("minute", 60);
69
+ if (absSeconds < 24 * 60 * 60) return format("hour", 60 * 60);
70
+ if (absSeconds < 7 * 24 * 60 * 60) return format("day", 24 * 60 * 60);
71
+ if (absSeconds < 30 * 24 * 60 * 60) return format("week", 7 * 24 * 60 * 60);
72
+ if (absSeconds < 365 * 24 * 60 * 60) return format("month", 30 * 24 * 60 * 60);
73
+ return format("year", 365 * 24 * 60 * 60);
74
+ }
75
+ function TimelineItemHeader({
76
+ title,
77
+ subtitle,
78
+ timestamp,
79
+ fallbackTimestampLabel,
80
+ icon,
81
+ color,
82
+ iconSize = "md",
83
+ className,
84
+ renderIcon,
85
+ renderColor
86
+ }) {
87
+ const wrapperSize = iconSize === "sm" ? "h-6 w-6" : "h-8 w-8";
88
+ const iconSizeClass = iconSize === "sm" ? "h-3.5 w-3.5" : "h-4 w-4";
89
+ const resolvedTimestamp = React.useMemo(() => {
90
+ if (subtitle) return subtitle;
91
+ if (!timestamp) return fallbackTimestampLabel ?? null;
92
+ const value = typeof timestamp === "string" ? timestamp : timestamp.toISOString();
93
+ const date = new Date(value);
94
+ if (Number.isNaN(date.getTime())) return fallbackTimestampLabel ?? null;
95
+ const now = Date.now();
96
+ const diff = Math.abs(now - date.getTime());
97
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
98
+ const relativeLabel = diff <= THIRTY_DAYS_MS ? formatRelativeTime(value) : null;
99
+ const absoluteLabel = formatDateTime(value);
100
+ if (relativeLabel) {
101
+ return /* @__PURE__ */ jsx("span", { title: absoluteLabel ?? void 0, children: relativeLabel });
102
+ }
103
+ return absoluteLabel ?? fallbackTimestampLabel ?? null;
104
+ }, [fallbackTimestampLabel, subtitle, timestamp]);
105
+ const iconNode = icon && renderIcon ? renderIcon(icon, iconSizeClass) : null;
106
+ return /* @__PURE__ */ jsxs("div", { className: ["flex items-start gap-3", className].filter(Boolean).join(" "), children: [
107
+ iconNode ? /* @__PURE__ */ jsx("span", { className: ["inline-flex items-center justify-center rounded border border-border bg-muted/40", wrapperSize].join(" "), children: iconNode }) : null,
108
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
109
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
110
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-foreground", children: title }),
111
+ color && renderColor ? renderColor(color, "h-3 w-3 rounded-full border border-border") : null
112
+ ] }),
113
+ resolvedTimestamp ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: resolvedTimestamp }) : null
114
+ ] })
115
+ ] });
116
+ }
117
+ function normalizeCustomFieldSubmitValue(value) {
118
+ if (Array.isArray(value)) {
119
+ return value.filter((entry) => entry !== void 0);
120
+ }
121
+ if (value === void 0) return null;
122
+ return value;
123
+ }
124
+ function buildActivityValidationError(errors, translate) {
125
+ const issue = errors[0];
126
+ if (!issue) {
127
+ throw createCrudFormError(translate("error", "Failed to save activity."));
128
+ }
129
+ const message = issue.message === INVALID_DATE_MESSAGE ? translate("invalidDate", "Invalid date") : translate("error", "Failed to save activity.");
130
+ const field = issue.path;
131
+ throw createCrudFormError(message, field ? { [field]: message } : void 0);
132
+ }
133
+ function ActivityForm({
134
+ mode,
135
+ initialValues,
136
+ onSubmit,
137
+ onCancel,
138
+ submitLabel,
139
+ cancelLabel,
140
+ isSubmitting = false,
141
+ activityTypeLabels,
142
+ loadActivityOptions,
143
+ createActivityOption,
144
+ dealOptions,
145
+ entityOptions,
146
+ defaultEntityId,
147
+ manageHref = "/backend/config/dictionaries",
148
+ customFieldEntityIds,
149
+ labelPrefix = "customers.people.detail.activities",
150
+ appearanceLabels
151
+ }) {
152
+ const tHook = useT();
153
+ const t = React.useMemo(() => createTranslatorWithFallback(tHook), [tHook]);
154
+ const translate = React.useCallback(
155
+ (suffix, fallback) => t(`${labelPrefix}.${suffix}`, fallback ?? ""),
156
+ [labelPrefix, t]
157
+ );
158
+ const [pending, setPending] = React.useState(false);
159
+ const normalizedDealOptions = React.useMemo(() => {
160
+ if (!Array.isArray(dealOptions)) return [];
161
+ const seen = /* @__PURE__ */ new Set();
162
+ return dealOptions.map((option) => {
163
+ if (!option || typeof option !== "object") return null;
164
+ const id = typeof option.id === "string" ? option.id.trim() : "";
165
+ if (!id || seen.has(id)) return null;
166
+ const label = typeof option.label === "string" && option.label.trim().length ? option.label.trim() : id;
167
+ seen.add(id);
168
+ return { id, label };
169
+ }).filter((option) => !!option);
170
+ }, [dealOptions]);
171
+ const normalizedEntityOptions = React.useMemo(() => {
172
+ if (!Array.isArray(entityOptions)) return [];
173
+ const seen = /* @__PURE__ */ new Set();
174
+ return entityOptions.map((option) => {
175
+ if (!option || typeof option !== "object") return null;
176
+ const id = typeof option.id === "string" ? option.id.trim() : "";
177
+ if (!id || seen.has(id)) return null;
178
+ const label = typeof option.label === "string" && option.label.trim().length ? option.label.trim() : id;
179
+ seen.add(id);
180
+ return { id, label };
181
+ }).filter((option) => !!option);
182
+ }, [entityOptions]);
183
+ const baseFields = React.useMemo(() => {
184
+ const fields = [];
185
+ if (normalizedEntityOptions.length) {
186
+ fields.push({
187
+ id: "entityId",
188
+ label: translate("fields.entity", "Assign to record"),
189
+ type: "custom",
190
+ layout: "half",
191
+ component: ({ value, setValue }) => {
192
+ const currentValue = typeof value === "string" && value.length ? value : normalizedEntityOptions[0]?.id ?? "";
193
+ return /* @__PURE__ */ jsx(
194
+ "select",
195
+ {
196
+ 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",
197
+ value: currentValue,
198
+ onChange: (event) => setValue(event.target.value),
199
+ children: normalizedEntityOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.label }, option.id))
200
+ }
201
+ );
202
+ }
203
+ });
204
+ }
205
+ if (normalizedDealOptions.length) {
206
+ fields.push({
207
+ id: "dealId",
208
+ label: translate("fields.deal", "Link to deal (optional)"),
209
+ type: "custom",
210
+ layout: "half",
211
+ component: ({ value, setValue }) => {
212
+ const currentValue = typeof value === "string" ? value : "";
213
+ return /* @__PURE__ */ jsxs(
214
+ "select",
215
+ {
216
+ 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",
217
+ value: currentValue,
218
+ onChange: (event) => setValue(event.target.value),
219
+ children: [
220
+ /* @__PURE__ */ jsx("option", { value: "", children: translate("fields.dealPlaceholder", "No linked deal") }),
221
+ normalizedDealOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.id, children: option.label }, option.id))
222
+ ]
223
+ }
224
+ );
225
+ }
226
+ });
227
+ }
228
+ fields.push({
229
+ id: "activityType",
230
+ label: translate("fields.type", "Activity type"),
231
+ type: "custom",
232
+ required: true,
233
+ layout: "half",
234
+ component: ({ value, setValue }) => /* @__PURE__ */ jsx(
235
+ DictionaryEntrySelect,
236
+ {
237
+ value: typeof value === "string" ? value : void 0,
238
+ onChange: (next) => setValue(next ?? ""),
239
+ fetchOptions: loadActivityOptions,
240
+ createOption: createActivityOption,
241
+ labels: activityTypeLabels,
242
+ allowAppearance: true,
243
+ allowInlineCreate: true,
244
+ appearanceLabels,
245
+ selectClassName: "w-full",
246
+ manageHref
247
+ }
248
+ )
249
+ });
250
+ fields.push({
251
+ id: "subject",
252
+ label: translate("fields.subject", "Subject"),
253
+ type: "text",
254
+ layout: "half",
255
+ placeholder: translate("subjectPlaceholder", "Add a subject (optional)")
256
+ });
257
+ fields.push({
258
+ id: "body",
259
+ label: translate("fields.body", "Details"),
260
+ type: "textarea",
261
+ placeholder: translate("bodyPlaceholder", "Describe the interaction")
262
+ });
263
+ fields.push({
264
+ id: "occurredAt",
265
+ label: translate("fields.occurredAt", "Occurred / will occur at"),
266
+ type: "custom",
267
+ component: ({ value, setValue }) => /* @__PURE__ */ jsx(
268
+ "input",
269
+ {
270
+ type: "datetime-local",
271
+ className: "w-full rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
272
+ value: typeof value === "string" ? value : "",
273
+ onChange: (event) => setValue(event.target.value || ""),
274
+ onFocus: (event) => {
275
+ const target = event.currentTarget;
276
+ if (typeof target.showPicker === "function") {
277
+ try {
278
+ target.showPicker();
279
+ } catch {
280
+ }
281
+ }
282
+ },
283
+ onClick: (event) => {
284
+ const target = event.currentTarget;
285
+ if (typeof target.showPicker === "function") {
286
+ try {
287
+ target.showPicker();
288
+ } catch {
289
+ }
290
+ }
291
+ }
292
+ }
293
+ ),
294
+ layout: "half"
295
+ });
296
+ return fields;
297
+ }, [
298
+ activityTypeLabels,
299
+ appearanceLabels,
300
+ createActivityOption,
301
+ loadActivityOptions,
302
+ manageHref,
303
+ normalizedDealOptions,
304
+ normalizedEntityOptions,
305
+ translate
306
+ ]);
307
+ const baseFieldIds = React.useMemo(() => new Set(baseFields.map((field) => field.id)), [baseFields]);
308
+ const groups = React.useMemo(() => {
309
+ const detailFields = [];
310
+ if (normalizedEntityOptions.length) detailFields.push("entityId");
311
+ if (normalizedDealOptions.length) detailFields.push("dealId");
312
+ detailFields.push("activityType", "subject", "occurredAt", "body");
313
+ const baseGroups = [
314
+ {
315
+ id: "details",
316
+ title: translate("form.details", "Activity details"),
317
+ column: 1,
318
+ fields: detailFields
319
+ }
320
+ ];
321
+ baseGroups.push({
322
+ id: "custom",
323
+ title: translate("form.customFields", "Custom fields"),
324
+ column: 2,
325
+ kind: "customFields"
326
+ });
327
+ return baseGroups;
328
+ }, [normalizedDealOptions.length, normalizedEntityOptions.length, translate]);
329
+ const handleSubmit = React.useCallback(
330
+ async (values) => {
331
+ if (pending || isSubmitting) return;
332
+ setPending(true);
333
+ try {
334
+ const parsed = schema.validate(values);
335
+ if (!parsed.ok) {
336
+ throw buildActivityValidationError(parsed.errors ?? [], translate);
337
+ }
338
+ const rawEntityId = typeof values.entityId === "string" ? values.entityId.trim() : "";
339
+ const resolvedEntityId = rawEntityId || (typeof defaultEntityId === "string" ? defaultEntityId : "");
340
+ const rawDealId = typeof values.dealId === "string" ? values.dealId.trim() : "";
341
+ const base = {
342
+ activityType: typeof values.activityType === "string" ? values.activityType.trim() : "",
343
+ subject: typeof values.subject === "string" && values.subject.trim().length ? values.subject.trim() : void 0,
344
+ body: typeof values.body === "string" && values.body.trim().length ? values.body.trim() : void 0,
345
+ occurredAt: typeof values.occurredAt === "string" && values.occurredAt.trim().length ? new Date(values.occurredAt).toISOString() : void 0,
346
+ dealId: rawDealId.length ? rawDealId : void 0
347
+ };
348
+ const reservedCustomKeys = /* @__PURE__ */ new Set(["entityId", "dealId"]);
349
+ const customEntries = collectCustomFieldValues(values, {
350
+ transform: (value) => normalizeCustomFieldSubmitValue(value),
351
+ accept: (fieldId) => !reservedCustomKeys.has(fieldId)
352
+ });
353
+ Object.entries(values).forEach(([key, value]) => {
354
+ if (key.startsWith("cf_")) return;
355
+ if (!baseFieldIds.has(key) && key !== "id") {
356
+ if (reservedCustomKeys.has(key)) return;
357
+ customEntries[key] = normalizeCustomFieldSubmitValue(value);
358
+ }
359
+ });
360
+ await onSubmit({ base, custom: customEntries, entityId: resolvedEntityId.length ? resolvedEntityId : void 0 });
361
+ } finally {
362
+ setPending(false);
363
+ }
364
+ },
365
+ [baseFieldIds, defaultEntityId, isSubmitting, onSubmit, pending, translate]
366
+ );
367
+ const embeddedInitialValues = React.useMemo(() => {
368
+ const occurredAt = toLocalDateTimeInput(initialValues?.occurredAt ?? null);
369
+ const resolvedEntity = (() => {
370
+ const raw = typeof initialValues?.entityId === "string" ? initialValues.entityId : typeof defaultEntityId === "string" ? defaultEntityId : normalizedEntityOptions[0]?.id ?? "";
371
+ return raw ?? "";
372
+ })();
373
+ const resolvedDeal = typeof initialValues?.dealId === "string" ? initialValues.dealId : "";
374
+ return {
375
+ entityId: resolvedEntity,
376
+ dealId: resolvedDeal,
377
+ activityType: initialValues?.activityType ?? "",
378
+ subject: initialValues?.subject ?? "",
379
+ body: initialValues?.body ?? "",
380
+ occurredAt,
381
+ ...Object.fromEntries(
382
+ Object.entries(initialValues ?? {}).filter(([key]) => {
383
+ if (!key.startsWith("cf_")) return false;
384
+ const trimmed = key.slice(3);
385
+ return trimmed !== "entityId" && trimmed !== "dealId";
386
+ }).map(([key, value]) => [key, value])
387
+ )
388
+ };
389
+ }, [defaultEntityId, initialValues, normalizedEntityOptions]);
390
+ return /* @__PURE__ */ jsx(
391
+ CrudForm,
392
+ {
393
+ embedded: true,
394
+ fields: baseFields,
395
+ groups,
396
+ initialValues: embeddedInitialValues,
397
+ onSubmit: handleSubmit,
398
+ submitLabel: submitLabel ?? (mode === "edit" ? translate("update", "Update activity (\u2318/Ctrl + Enter)") : translate("save", "Save activity (\u2318/Ctrl + Enter)")),
399
+ extraActions: /* @__PURE__ */ jsx(
400
+ Button,
401
+ {
402
+ type: "button",
403
+ variant: "outline",
404
+ onClick: onCancel,
405
+ disabled: pending || isSubmitting,
406
+ children: cancelLabel ?? translate("cancel", "Cancel")
407
+ }
408
+ ),
409
+ entityIds: customFieldEntityIds
410
+ }
411
+ );
412
+ }
413
+ function ActivityDialog({
414
+ open,
415
+ mode,
416
+ onOpenChange,
417
+ initialValues,
418
+ onSubmit,
419
+ isSubmitting,
420
+ activityTypeLabels,
421
+ loadActivityOptions,
422
+ createActivityOption,
423
+ titles,
424
+ submitLabels,
425
+ cancelLabel,
426
+ dealOptions,
427
+ entityOptions,
428
+ defaultEntityId,
429
+ manageHref,
430
+ customFieldEntityIds,
431
+ labelPrefix = "customers.people.detail.activities",
432
+ appearanceLabels
433
+ }) {
434
+ const tHook = useT();
435
+ const t = React.useMemo(() => createTranslatorWithFallback(tHook), [tHook]);
436
+ const translate = React.useCallback(
437
+ (suffix, fallback, params) => t(`${labelPrefix}.${suffix}`, fallback ?? "", params),
438
+ [labelPrefix, t]
439
+ );
440
+ const dialogTitle = mode === "edit" ? titles?.edit ?? translate("editTitle", "Edit activity") : titles?.create ?? translate("addTitle", "Add activity");
441
+ const resolvedSubmitLabel = mode === "edit" ? submitLabels?.edit ?? translate("update", "Update activity (\u2318/Ctrl + Enter)") : submitLabels?.create ?? translate("save", "Save activity (\u2318/Ctrl + Enter)");
442
+ const resolvedCancelLabel = cancelLabel ?? translate("cancel", "Cancel");
443
+ const handleCancel = React.useCallback(() => {
444
+ onOpenChange(false);
445
+ }, [onOpenChange]);
446
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-3xl", children: [
447
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: dialogTitle }) }),
448
+ /* @__PURE__ */ jsx(
449
+ ActivityForm,
450
+ {
451
+ mode,
452
+ initialValues,
453
+ onSubmit,
454
+ onCancel: handleCancel,
455
+ submitLabel: resolvedSubmitLabel,
456
+ cancelLabel: resolvedCancelLabel,
457
+ isSubmitting,
458
+ activityTypeLabels,
459
+ loadActivityOptions,
460
+ createActivityOption,
461
+ dealOptions,
462
+ entityOptions,
463
+ defaultEntityId,
464
+ manageHref,
465
+ customFieldEntityIds,
466
+ labelPrefix,
467
+ appearanceLabels
468
+ }
469
+ )
470
+ ] }) });
471
+ }
472
+ function ActivitiesSection({
473
+ entityId,
474
+ dealId,
475
+ addActionLabel,
476
+ emptyState,
477
+ onActionChange,
478
+ onLoadingChange,
479
+ dealOptions,
480
+ entityOptions,
481
+ defaultEntityId,
482
+ dataAdapter,
483
+ dataContext,
484
+ activityTypeLabels,
485
+ loadActivityOptions,
486
+ createActivityOption,
487
+ resolveActivityPresentation,
488
+ renderCustomFields,
489
+ customFieldEntityIds,
490
+ labelPrefix = "customers.people.detail.activities",
491
+ renderIcon,
492
+ renderColor,
493
+ appearanceLabels,
494
+ dealLinkHref,
495
+ manageHref
496
+ }) {
497
+ const tHook = useT();
498
+ const baseTranslator = React.useMemo(() => createTranslatorWithFallback(tHook), [tHook]);
499
+ const translate = React.useCallback(
500
+ (suffix, fallback, params) => baseTranslator(`${labelPrefix}.${suffix}`, fallback ?? "", params),
501
+ [baseTranslator, labelPrefix]
502
+ );
503
+ const resolvedDefaultEntityId = React.useMemo(() => {
504
+ const primary = typeof entityId === "string" ? entityId.trim() : "";
505
+ if (primary.length) return primary;
506
+ const fallback = typeof defaultEntityId === "string" ? defaultEntityId.trim() : "";
507
+ if (fallback.length) return fallback;
508
+ if (Array.isArray(entityOptions)) {
509
+ for (const option of entityOptions) {
510
+ if (!option || typeof option !== "object") continue;
511
+ const id = typeof option.id === "string" ? option.id.trim() : "";
512
+ if (id.length) return id;
513
+ }
514
+ }
515
+ return "";
516
+ }, [defaultEntityId, entityId, entityOptions]);
517
+ const resolveEntityForSubmission = React.useCallback(
518
+ (input) => {
519
+ const candidate = typeof input === "string" ? input.trim() : "";
520
+ if (candidate.length) return candidate;
521
+ return resolvedDefaultEntityId.length ? resolvedDefaultEntityId : null;
522
+ },
523
+ [resolvedDefaultEntityId]
524
+ );
525
+ const [activities, setActivities] = React.useState([]);
526
+ const [isLoading, setIsLoading] = React.useState(() => {
527
+ const entity = typeof entityId === "string" ? entityId.trim() : "";
528
+ const deal = typeof dealId === "string" ? dealId.trim() : "";
529
+ return Boolean(entity || deal || resolvedDefaultEntityId);
530
+ });
531
+ const [loadError, setLoadError] = React.useState(null);
532
+ const [pendingAction, setPendingAction] = React.useState(null);
533
+ const [dialogOpen, setDialogOpen] = React.useState(false);
534
+ const [dialogMode, setDialogMode] = React.useState("create");
535
+ const [editingActivityId, setEditingActivityId] = React.useState(null);
536
+ const [initialValues, setInitialValues] = React.useState(void 0);
537
+ const [visibleCount, setVisibleCount] = React.useState(0);
538
+ const pendingCounterRef = React.useRef(0);
539
+ const t = translate;
540
+ const pushLoading = React.useCallback(() => {
541
+ pendingCounterRef.current += 1;
542
+ if (pendingCounterRef.current === 1) {
543
+ onLoadingChange?.(true);
544
+ }
545
+ }, [onLoadingChange]);
546
+ const popLoading = React.useCallback(() => {
547
+ pendingCounterRef.current = Math.max(0, pendingCounterRef.current - 1);
548
+ if (pendingCounterRef.current === 0) {
549
+ onLoadingChange?.(false);
550
+ }
551
+ }, [onLoadingChange]);
552
+ const updateVisibleCount = React.useCallback((length) => {
553
+ if (!length) {
554
+ setVisibleCount(0);
555
+ return;
556
+ }
557
+ const baseline = Math.min(5, length);
558
+ setVisibleCount((prev) => {
559
+ if (prev >= length) {
560
+ return Math.min(prev, length);
561
+ }
562
+ return Math.min(Math.max(prev, baseline), length);
563
+ });
564
+ }, []);
565
+ const loadActivities = React.useCallback(async () => {
566
+ const queryEntityId = typeof entityId === "string" ? entityId.trim() : "";
567
+ const queryDealId = typeof dealId === "string" ? dealId.trim() : "";
568
+ if (!queryEntityId && !queryDealId) {
569
+ setActivities([]);
570
+ setLoadError(null);
571
+ updateVisibleCount(0);
572
+ return;
573
+ }
574
+ pushLoading();
575
+ setIsLoading(true);
576
+ try {
577
+ const items = await dataAdapter.list({
578
+ entityId: queryEntityId || null,
579
+ dealId: queryDealId || null,
580
+ context: dataContext
581
+ });
582
+ setActivities(items);
583
+ setLoadError(null);
584
+ updateVisibleCount(items.length);
585
+ } catch (err) {
586
+ const message = err instanceof Error ? err.message : t("loadError", "Failed to load activities.");
587
+ setLoadError(message);
588
+ } finally {
589
+ setIsLoading(false);
590
+ popLoading();
591
+ }
592
+ }, [dataAdapter, dataContext, dealId, entityId, popLoading, pushLoading, t, updateVisibleCount]);
593
+ React.useEffect(() => {
594
+ updateVisibleCount(activities.length);
595
+ }, [activities.length, updateVisibleCount]);
596
+ React.useEffect(() => {
597
+ const queryEntityId = typeof entityId === "string" ? entityId.trim() : "";
598
+ const queryDealId = typeof dealId === "string" ? dealId.trim() : "";
599
+ if (!queryEntityId && !queryDealId) {
600
+ setActivities([]);
601
+ setLoadError(null);
602
+ setIsLoading(false);
603
+ pendingCounterRef.current = 0;
604
+ onLoadingChange?.(false);
605
+ updateVisibleCount(0);
606
+ return;
607
+ }
608
+ loadActivities().catch(() => {
609
+ });
610
+ }, [dealId, entityId, loadActivities, onLoadingChange, updateVisibleCount]);
611
+ const openCreateDialog = React.useCallback(() => {
612
+ setDialogMode("create");
613
+ setEditingActivityId(null);
614
+ setInitialValues(void 0);
615
+ setDialogOpen(true);
616
+ }, []);
617
+ const openEditDialog = React.useCallback((activity) => {
618
+ setDialogMode("edit");
619
+ setEditingActivityId(activity.id);
620
+ const baseValues = {
621
+ activityType: activity.activityType,
622
+ subject: activity.subject ?? "",
623
+ body: activity.body ?? "",
624
+ occurredAt: activity.occurredAt ?? activity.createdAt ?? null,
625
+ dealId: activity.dealId ?? "",
626
+ entityId: activity.entityId ?? ""
627
+ };
628
+ const customEntries = Array.isArray(activity.customFields) ? activity.customFields : [];
629
+ customEntries.forEach((entry) => {
630
+ if (entry.key === "entityId" || entry.key === "dealId") return;
631
+ baseValues[`cf_${entry.key}`] = entry.value ?? null;
632
+ });
633
+ setInitialValues(baseValues);
634
+ setDialogOpen(true);
635
+ }, []);
636
+ const closeDialog = React.useCallback(() => {
637
+ setDialogOpen(false);
638
+ setDialogMode("create");
639
+ setEditingActivityId(null);
640
+ setInitialValues(void 0);
641
+ }, []);
642
+ const handleDialogOpenChange = React.useCallback(
643
+ (next) => {
644
+ if (!next) {
645
+ closeDialog();
646
+ } else {
647
+ setDialogOpen(true);
648
+ }
649
+ },
650
+ [closeDialog]
651
+ );
652
+ const handleCreate = React.useCallback(
653
+ async ({ base, custom, entityId: formEntityId }) => {
654
+ const submissionEntityId = resolveEntityForSubmission(formEntityId);
655
+ if (!submissionEntityId) {
656
+ const message = t("entityMissing", "Select a related record before saving.");
657
+ flash(message, "error");
658
+ throw new Error(message);
659
+ }
660
+ setPendingAction({ kind: "create" });
661
+ pushLoading();
662
+ try {
663
+ const payload = {
664
+ entityId: submissionEntityId,
665
+ activityType: base.activityType,
666
+ subject: base.subject ?? void 0,
667
+ body: base.body ?? void 0,
668
+ occurredAt: base.occurredAt ?? void 0,
669
+ dealId: base.dealId ?? void 0,
670
+ customFields: Object.keys(custom).length ? custom : void 0
671
+ };
672
+ await dataAdapter.create({ ...payload, context: dataContext });
673
+ await loadActivities();
674
+ flash(t("success", "Activity saved"), "success");
675
+ } catch (err) {
676
+ const message = err instanceof Error ? err.message : t("error", "Failed to save activity");
677
+ throw err instanceof Error ? err : new Error(message);
678
+ } finally {
679
+ setPendingAction(null);
680
+ popLoading();
681
+ }
682
+ },
683
+ [dataAdapter, dataContext, loadActivities, popLoading, pushLoading, resolveEntityForSubmission, t]
684
+ );
685
+ const handleUpdate = React.useCallback(
686
+ async (activityId, { base, custom, entityId: formEntityId }) => {
687
+ const submissionEntityId = resolveEntityForSubmission(formEntityId);
688
+ if (!submissionEntityId) {
689
+ const message = t("entityMissing", "Select a related record before saving.");
690
+ flash(message, "error");
691
+ throw new Error(message);
692
+ }
693
+ setPendingAction({ kind: "update", id: activityId });
694
+ pushLoading();
695
+ try {
696
+ const patch = {
697
+ entityId: submissionEntityId,
698
+ activityType: base.activityType,
699
+ subject: base.subject ?? void 0,
700
+ body: base.body ?? void 0,
701
+ occurredAt: base.occurredAt ?? void 0,
702
+ dealId: base.dealId ?? void 0,
703
+ customFields: Object.keys(custom).length ? custom : void 0
704
+ };
705
+ await dataAdapter.update({ id: activityId, patch, context: dataContext });
706
+ await loadActivities();
707
+ flash(t("updateSuccess", "Activity updated."), "success");
708
+ } catch (err) {
709
+ const message = err instanceof Error ? err.message : t("error", "Failed to save activity");
710
+ throw err instanceof Error ? err : new Error(message);
711
+ } finally {
712
+ setPendingAction(null);
713
+ popLoading();
714
+ }
715
+ },
716
+ [dataAdapter, dataContext, loadActivities, popLoading, pushLoading, resolveEntityForSubmission, t]
717
+ );
718
+ const handleDelete = React.useCallback(
719
+ async (activity) => {
720
+ if (!activity.id) return;
721
+ const confirmed = typeof window === "undefined" ? true : window.confirm(
722
+ t(
723
+ "deleteConfirm",
724
+ "Delete this activity? This action cannot be undone."
725
+ )
726
+ );
727
+ if (!confirmed) return;
728
+ setPendingAction({ kind: "delete", id: activity.id });
729
+ try {
730
+ await dataAdapter.delete({ id: activity.id, context: dataContext });
731
+ setActivities((prev) => prev.filter((existing) => existing.id !== activity.id));
732
+ flash(t("deleteSuccess", "Activity deleted."), "success");
733
+ } catch (err) {
734
+ const message = err instanceof Error ? err.message : t("deleteError", "Failed to delete activity.");
735
+ flash(message, "error");
736
+ throw err instanceof Error ? err : new Error(message);
737
+ } finally {
738
+ setPendingAction(null);
739
+ }
740
+ },
741
+ [dataAdapter, dataContext, t]
742
+ );
743
+ const handleDialogSubmit = React.useCallback(
744
+ async (payload) => {
745
+ if (dialogMode === "edit" && editingActivityId) {
746
+ await handleUpdate(editingActivityId, payload);
747
+ } else {
748
+ await handleCreate(payload);
749
+ }
750
+ closeDialog();
751
+ },
752
+ [closeDialog, dialogMode, editingActivityId, handleCreate, handleUpdate]
753
+ );
754
+ React.useEffect(() => {
755
+ if (!onActionChange) return;
756
+ if (activities.length === 0) {
757
+ onActionChange(null);
758
+ return () => {
759
+ onActionChange(null);
760
+ };
761
+ }
762
+ const disabled = resolveEntityForSubmission(null) === null || pendingAction !== null || isLoading;
763
+ const action = {
764
+ label: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
765
+ /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }),
766
+ addActionLabel
767
+ ] }),
768
+ onClick: () => {
769
+ if (!disabled) openCreateDialog();
770
+ },
771
+ disabled
772
+ };
773
+ onActionChange(action);
774
+ return () => {
775
+ onActionChange(null);
776
+ };
777
+ }, [
778
+ activities.length,
779
+ addActionLabel,
780
+ isLoading,
781
+ onActionChange,
782
+ openCreateDialog,
783
+ pendingAction,
784
+ resolveEntityForSubmission
785
+ ]);
786
+ const isFormPending = pendingAction?.kind === "create" || pendingAction?.kind === "update" && pendingAction.id === editingActivityId;
787
+ const visibleActivities = React.useMemo(
788
+ () => activities.slice(0, visibleCount),
789
+ [activities, visibleCount]
790
+ );
791
+ const hasMoreActivities = visibleCount < activities.length;
792
+ const loadMoreLabel = t("loadMore", "Load more activities");
793
+ const handleLoadMore = React.useCallback(() => {
794
+ setVisibleCount((prev) => {
795
+ if (prev >= activities.length) return prev;
796
+ return Math.min(prev + 5, activities.length);
797
+ });
798
+ }, [activities.length]);
799
+ const resolvePresentation = React.useCallback(
800
+ (activity) => {
801
+ if (resolveActivityPresentation) return resolveActivityPresentation(activity);
802
+ return {
803
+ label: activity.activityType,
804
+ icon: activity.appearanceIcon ?? null,
805
+ color: activity.appearanceColor ?? null
806
+ };
807
+ },
808
+ [resolveActivityPresentation]
809
+ );
810
+ const resolveDealHref = React.useCallback(
811
+ (id) => dealLinkHref ? dealLinkHref(id) : `/backend/customers/deals/${encodeURIComponent(id)}`,
812
+ [dealLinkHref]
813
+ );
814
+ return /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-4", children: [
815
+ loadError ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: loadError }) : null,
816
+ /* @__PURE__ */ jsx("div", { className: "space-y-4", children: isLoading && activities.length === 0 ? /* @__PURE__ */ jsx(
817
+ LoadingMessage,
818
+ {
819
+ label: t("loading", "Loading activities\u2026"),
820
+ className: "border-0 bg-transparent p-0 py-8 justify-center"
821
+ }
822
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
823
+ !isLoading && activities.length === 0 && !dialogOpen ? /* @__PURE__ */ jsx(
824
+ TabEmptyState,
825
+ {
826
+ title: emptyState.title,
827
+ action: {
828
+ label: emptyState.actionLabel,
829
+ onClick: openCreateDialog,
830
+ disabled: resolveEntityForSubmission(null) === null || pendingAction !== null
831
+ }
832
+ }
833
+ ) : null,
834
+ visibleActivities.length > 0 ? visibleActivities.map((activity) => {
835
+ const presentation = resolvePresentation(activity);
836
+ const timestampValue = activity.occurredAt ?? activity.createdAt ?? null;
837
+ const occurredLabel = formatDateTime(timestampValue) ?? t("noDate", "No date provided");
838
+ const authorLabel = activity.authorName ?? activity.authorEmail ?? null;
839
+ const loggedByText = authorLabel ? (() => {
840
+ const translated = t("loggedBy", `Logged by ${authorLabel}`, { user: authorLabel });
841
+ if (!translated || translated.includes("{{") || translated.includes("{user")) {
842
+ return `Logged by ${authorLabel}`;
843
+ }
844
+ return translated;
845
+ })() : null;
846
+ const isUpdatePending = pendingAction?.kind === "update" && pendingAction.id === activity.id;
847
+ const isDeletePending = pendingAction?.kind === "delete" && pendingAction.id === activity.id;
848
+ return /* @__PURE__ */ jsxs(
849
+ "div",
850
+ {
851
+ className: "group space-y-3 rounded-lg border bg-card p-4 transition hover:border-border/80 cursor-pointer",
852
+ role: "button",
853
+ tabIndex: 0,
854
+ onClick: () => openEditDialog(activity),
855
+ onKeyDown: (event) => {
856
+ if (event.key === "Enter" || event.key === " ") {
857
+ event.preventDefault();
858
+ openEditDialog(activity);
859
+ }
860
+ },
861
+ children: [
862
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-start justify-between gap-3", children: [
863
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
864
+ /* @__PURE__ */ jsx(
865
+ TimelineItemHeader,
866
+ {
867
+ title: presentation.label,
868
+ timestamp: timestampValue,
869
+ fallbackTimestampLabel: occurredLabel,
870
+ icon: presentation.icon,
871
+ color: presentation.color,
872
+ renderIcon,
873
+ renderColor
874
+ }
875
+ ),
876
+ activity.dealId ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
877
+ /* @__PURE__ */ jsx(ArrowUpRightSquare, { className: "h-3.5 w-3.5" }),
878
+ /* @__PURE__ */ jsx(
879
+ Link,
880
+ {
881
+ href: resolveDealHref(activity.dealId),
882
+ className: "font-medium text-foreground hover:underline",
883
+ onClick: (event) => event.stopPropagation(),
884
+ children: activity.dealTitle && activity.dealTitle.length ? activity.dealTitle : t("linkedDeal", "Linked deal")
885
+ }
886
+ )
887
+ ] }) : null
888
+ ] }),
889
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100", children: [
890
+ /* @__PURE__ */ jsx(
891
+ Button,
892
+ {
893
+ type: "button",
894
+ variant: "ghost",
895
+ size: "icon",
896
+ onClick: (event) => {
897
+ event.stopPropagation();
898
+ openEditDialog(activity);
899
+ },
900
+ disabled: pendingAction !== null,
901
+ children: isUpdatePending ? /* @__PURE__ */ jsx("span", { className: "relative flex h-4 w-4 items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "absolute h-4 w-4 animate-spin rounded-full border border-primary border-t-transparent" }) }) : /* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4" })
902
+ }
903
+ ),
904
+ /* @__PURE__ */ jsx(
905
+ Button,
906
+ {
907
+ type: "button",
908
+ variant: "ghost",
909
+ size: "icon",
910
+ onClick: (event) => {
911
+ event.stopPropagation();
912
+ handleDelete(activity).catch(() => {
913
+ });
914
+ },
915
+ disabled: pendingAction !== null,
916
+ children: isDeletePending ? /* @__PURE__ */ jsx("span", { className: "relative flex h-4 w-4 items-center justify-center text-destructive", children: /* @__PURE__ */ jsx("span", { className: "absolute h-4 w-4 animate-spin rounded-full border border-destructive border-t-transparent" }) }) : /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
917
+ }
918
+ )
919
+ ] })
920
+ ] }),
921
+ activity.subject ? /* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: activity.subject }) : null,
922
+ activity.body ? /* @__PURE__ */ jsx("p", { className: "text-sm whitespace-pre-wrap text-muted-foreground", children: activity.body }) : null,
923
+ renderCustomFields ? renderCustomFields(activity) : null,
924
+ loggedByText ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: loggedByText }) : null
925
+ ]
926
+ },
927
+ activity.id
928
+ );
929
+ }) : null,
930
+ hasMoreActivities ? /* @__PURE__ */ jsx("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleLoadMore, disabled: pendingAction !== null, children: loadMoreLabel }) }) : null
931
+ ] }) }),
932
+ /* @__PURE__ */ jsx(
933
+ ActivityDialog,
934
+ {
935
+ open: dialogOpen,
936
+ mode: dialogMode,
937
+ onOpenChange: handleDialogOpenChange,
938
+ initialValues,
939
+ onSubmit: async (payload) => {
940
+ await handleDialogSubmit(payload);
941
+ },
942
+ isSubmitting: Boolean(isFormPending),
943
+ activityTypeLabels,
944
+ loadActivityOptions,
945
+ createActivityOption,
946
+ dealOptions,
947
+ entityOptions,
948
+ defaultEntityId: resolvedDefaultEntityId || void 0,
949
+ manageHref,
950
+ customFieldEntityIds,
951
+ labelPrefix,
952
+ appearanceLabels
953
+ }
954
+ )
955
+ ] });
956
+ }
957
+ var ActivitiesSection_default = ActivitiesSection;
958
+ export {
959
+ ActivitiesSection,
960
+ ActivitiesSection_default as default
961
+ };
962
+ //# sourceMappingURL=ActivitiesSection.js.map