@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30

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 (243) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
  3. package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
  4. package/dist/generated/entities/customer_interaction/index.js +2 -0
  5. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/catalog/commands/categories.js +1 -1
  9. package/dist/modules/catalog/commands/categories.js.map +2 -2
  10. package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
  11. package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
  12. package/dist/modules/catalog/commands/products.js +52 -17
  13. package/dist/modules/catalog/commands/products.js.map +2 -2
  14. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
  15. package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
  16. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
  17. package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
  18. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
  19. package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
  20. package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
  21. package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
  22. package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
  23. package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
  24. package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
  25. package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
  26. package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
  27. package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
  28. package/dist/modules/customers/api/activity-types/route.js +151 -0
  29. package/dist/modules/customers/api/activity-types/route.js.map +7 -0
  30. package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
  31. package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
  32. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
  33. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  34. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
  35. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  36. package/dist/modules/customers/api/interactions/route.js +3 -0
  37. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  38. package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
  39. package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
  40. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
  41. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
  42. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
  43. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  44. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
  45. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  46. package/dist/modules/customers/calendar-event-types.js +548 -0
  47. package/dist/modules/customers/calendar-event-types.js.map +7 -0
  48. package/dist/modules/customers/commands/dictionaries.js +70 -10
  49. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  50. package/dist/modules/customers/commands/interactions.js +187 -22
  51. package/dist/modules/customers/commands/interactions.js.map +3 -3
  52. package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
  53. package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
  54. package/dist/modules/customers/components/DictionarySettings.js +3 -1
  55. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  56. package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
  57. package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
  58. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
  59. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  60. package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
  61. package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
  62. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
  63. package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
  64. package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
  65. package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
  66. package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
  67. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  68. package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
  69. package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
  70. package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
  71. package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
  72. package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
  73. package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
  74. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
  75. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  76. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
  77. package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
  78. package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
  79. package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
  80. package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
  81. package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
  82. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
  83. package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
  84. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
  85. package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
  86. package/dist/modules/customers/components/calendar/types.js +1 -0
  87. package/dist/modules/customers/components/calendar/types.js.map +2 -2
  88. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
  89. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  90. package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
  91. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
  92. package/dist/modules/customers/components/detail/DealForm.js +7 -3
  93. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  94. package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
  95. package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
  96. package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
  97. package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
  98. package/dist/modules/customers/components/detail/PersonCard.js +5 -2
  99. package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
  100. package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
  101. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
  102. package/dist/modules/customers/data/entities.js +6 -0
  103. package/dist/modules/customers/data/entities.js.map +2 -2
  104. package/dist/modules/customers/data/validators.js +39 -10
  105. package/dist/modules/customers/data/validators.js.map +2 -2
  106. package/dist/modules/customers/di.js +20 -1
  107. package/dist/modules/customers/di.js.map +2 -2
  108. package/dist/modules/customers/extension-points.js +13 -0
  109. package/dist/modules/customers/extension-points.js.map +2 -2
  110. package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
  111. package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
  112. package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
  113. package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
  114. package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
  115. package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
  116. package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
  117. package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
  118. package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
  119. package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
  120. package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
  121. package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
  122. package/dist/modules/customers/lib/calendar/timezone.js +111 -0
  123. package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
  124. package/dist/modules/customers/lib/interactionReadModel.js +1 -0
  125. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  126. package/dist/modules/customers/lib/todoCompatibility.js +1 -0
  127. package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
  128. package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
  129. package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
  130. package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
  131. package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
  132. package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
  133. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  134. package/dist/modules/messages/subscribers/message-notification.js +13 -1
  135. package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
  136. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
  137. package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
  138. package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
  139. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  140. package/dist/modules/planner/lib/availabilityMerge.js +101 -38
  141. package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
  142. package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
  143. package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
  144. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  145. package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
  146. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
  148. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  149. package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
  150. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  151. package/generated/entities/customer_dictionary_entry/index.ts +1 -0
  152. package/generated/entities/customer_interaction/index.ts +1 -0
  153. package/generated/entity-fields-registry.ts +2 -0
  154. package/package.json +9 -8
  155. package/src/modules/auth/i18n/de.json +3 -0
  156. package/src/modules/auth/i18n/en.json +3 -0
  157. package/src/modules/auth/i18n/es.json +3 -0
  158. package/src/modules/auth/i18n/ko.json +3 -0
  159. package/src/modules/auth/i18n/pl.json +3 -0
  160. package/src/modules/catalog/commands/categories.ts +1 -1
  161. package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
  162. package/src/modules/catalog/commands/products.ts +58 -17
  163. package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
  164. package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
  165. package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
  166. package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
  167. package/src/modules/communication_channels/i18n/de.json +4 -0
  168. package/src/modules/communication_channels/i18n/en.json +4 -0
  169. package/src/modules/communication_channels/i18n/es.json +4 -0
  170. package/src/modules/communication_channels/i18n/ko.json +4 -0
  171. package/src/modules/communication_channels/i18n/pl.json +4 -0
  172. package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
  173. package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
  174. package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
  175. package/src/modules/customers/api/activity-types/route.ts +154 -0
  176. package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
  177. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
  178. package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
  179. package/src/modules/customers/api/interactions/route.ts +4 -0
  180. package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
  181. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
  182. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
  183. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
  184. package/src/modules/customers/calendar-event-types.ts +674 -0
  185. package/src/modules/customers/commands/dictionaries.ts +84 -8
  186. package/src/modules/customers/commands/interactions.ts +244 -21
  187. package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
  188. package/src/modules/customers/components/DictionarySettings.tsx +5 -3
  189. package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
  190. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
  191. package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
  192. package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
  193. package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
  194. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
  195. package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
  196. package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
  197. package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
  198. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
  199. package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
  200. package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
  201. package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
  202. package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
  203. package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
  204. package/src/modules/customers/components/calendar/types.ts +1 -0
  205. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
  206. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
  207. package/src/modules/customers/components/detail/DealForm.tsx +7 -3
  208. package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
  209. package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
  210. package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
  211. package/src/modules/customers/components/detail/types.ts +1 -0
  212. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
  213. package/src/modules/customers/data/entities.ts +8 -1
  214. package/src/modules/customers/data/validators.ts +42 -7
  215. package/src/modules/customers/di.ts +21 -0
  216. package/src/modules/customers/extension-points.ts +13 -0
  217. package/src/modules/customers/i18n/de.json +104 -8
  218. package/src/modules/customers/i18n/en.json +104 -8
  219. package/src/modules/customers/i18n/es.json +104 -8
  220. package/src/modules/customers/i18n/ko.json +104 -8
  221. package/src/modules/customers/i18n/pl.json +104 -8
  222. package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
  223. package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
  224. package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
  225. package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
  226. package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
  227. package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
  228. package/src/modules/customers/lib/calendar/timezone.ts +117 -0
  229. package/src/modules/customers/lib/interactionReadModel.ts +1 -0
  230. package/src/modules/customers/lib/todoCompatibility.ts +2 -0
  231. package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
  232. package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
  233. package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
  234. package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
  235. package/src/modules/messages/subscribers/message-notification.ts +13 -3
  236. package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
  237. package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
  238. package/src/modules/planner/lib/availabilityMerge.ts +106 -41
  239. package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
  240. package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
  241. package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
  242. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
  243. package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
@@ -1,27 +1,24 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { Users, Link2, Plus, Filter } from 'lucide-react'
5
- import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
6
- import { Button } from '@open-mercato/ui/primitives/button'
7
- import { Badge } from '@open-mercato/ui/primitives/badge'
8
4
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
5
  import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
10
- import {
11
- readVersionedIdSet,
12
- writeVersionedIdSet,
13
- } from '@open-mercato/shared/lib/browser/versionedPreference'
14
6
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
7
  import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
16
8
  import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
17
9
  import type { AppEventPayload } from '@open-mercato/shared/modules/widgets/injection'
18
10
  import type { SectionAction, TabEmptyStateConfig, Translator } from './types'
19
11
  import { CreatePersonDialog } from './CreatePersonDialog'
20
- import { PersonCard } from './PersonCard'
21
12
  import { coerceDisplayName } from '../../lib/displayName'
22
13
  import { DecisionMakersFooter } from './DecisionMakersFooter'
23
14
  import { RolesSection } from './RolesSection'
24
- import { LinkEntityDialog, type LinkEntityOption } from '../linking/LinkEntityDialog'
15
+ import {
16
+ LinkedPeopleSection,
17
+ type LinkedPeoplePage,
18
+ type LinkedPeopleSortMode,
19
+ type LinkedPersonSummary,
20
+ } from './LinkedPeopleSection'
21
+ import type { LinkEntityOption } from '../linking/LinkEntityDialog'
25
22
  import { createPersonLinkAdapter } from '../linking/adapters/personAdapter'
26
23
 
27
24
  type GuardedMutationRunner = <T>(
@@ -29,21 +26,7 @@ type GuardedMutationRunner = <T>(
29
26
  mutationPayload?: Record<string, unknown>,
30
27
  ) => Promise<T>
31
28
 
32
- export type CompanyPersonSummary = {
33
- id: string
34
- displayName: string
35
- primaryEmail?: string | null
36
- primaryPhone?: string | null
37
- status?: string | null
38
- lifecycleStage?: string | null
39
- jobTitle?: string | null
40
- department?: string | null
41
- createdAt?: string | null
42
- organizationId?: string | null
43
- temperature?: string | null
44
- source?: string | null
45
- linkedAt?: string | null
46
- }
29
+ export type CompanyPersonSummary = LinkedPersonSummary
47
30
 
48
31
  export type CompanyPeopleSectionProps = {
49
32
  companyId: string
@@ -60,9 +43,6 @@ export type CompanyPeopleSectionProps = {
60
43
  runGuardedMutation?: GuardedMutationRunner
61
44
  }
62
45
 
63
- const COMPANY_PEOPLE_PAGE_SIZE = 20
64
- const STARRED_PEOPLE_STORAGE_VERSION = 1
65
-
66
46
  function normalizeCompanyPerson(record: Record<string, unknown>): CompanyPersonSummary | null {
67
47
  const id = typeof record.id === 'string' ? record.id : null
68
48
  if (!id) return null
@@ -160,7 +140,7 @@ function matchesCompanyPersonSearch(person: CompanyPersonSummary, query: string)
160
140
 
161
141
  function sortCompanyPeople(
162
142
  items: CompanyPersonSummary[],
163
- sortMode: 'name-asc' | 'name-desc' | 'recent',
143
+ sortMode: LinkedPeopleSortMode,
164
144
  ): CompanyPersonSummary[] {
165
145
  return [...items].sort((left, right) => {
166
146
  if (sortMode === 'recent') {
@@ -196,22 +176,15 @@ export function CompanyPeopleSection({
196
176
  )
197
177
  const translate: Translator = translator ?? fallbackTranslator
198
178
  const [people, setPeople] = React.useState<CompanyPersonSummary[]>(initialPeople)
199
- const [removingId, setRemovingId] = React.useState<string | null>(null)
200
- const [linkDialogOpen, setLinkDialogOpen] = React.useState(false)
201
179
  const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
202
- const [searchQuery, setSearchQuery] = React.useState('')
203
- const [sortMode, setSortMode] = React.useState<'name-asc' | 'name-desc' | 'recent'>('name-asc')
204
- const [filtersOpen, setFiltersOpen] = React.useState(true)
205
- const [visiblePeople, setVisiblePeople] = React.useState<CompanyPersonSummary[]>([])
206
- const [listPage, setListPage] = React.useState(1)
207
- const [listTotalPages, setListTotalPages] = React.useState(1)
208
- const [listTotalCount, setListTotalCount] = React.useState(initialPeople.length)
209
- const [listLoading, setListLoading] = React.useState(true)
210
- const [starredIds, setStarredIds] = React.useState<Set<string>>(
211
- () => readVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION),
212
- )
180
+ const [linkDialogOpen, setLinkDialogOpen] = React.useState(false)
181
+ const [refreshKey, setRefreshKey] = React.useState(0)
213
182
  const pendingPeopleChangeRef = React.useRef(false)
214
183
 
184
+ const requestRefresh = React.useCallback(() => {
185
+ setRefreshKey((current) => current + 1)
186
+ }, [])
187
+
215
188
  const runWriteMutation = React.useCallback(
216
189
  async <T,>(
217
190
  operation: () => Promise<T>,
@@ -225,32 +198,6 @@ export function CompanyPeopleSection({
225
198
  [runGuardedMutation],
226
199
  )
227
200
 
228
- const toggleStar = React.useCallback(
229
- (personId: string) => {
230
- setStarredIds((prev) => {
231
- const next = new Set(prev)
232
- if (next.has(personId)) next.delete(personId)
233
- else next.add(personId)
234
- writeVersionedIdSet(`om:starred-people:${companyId}`, STARRED_PEOPLE_STORAGE_VERSION, next)
235
- return next
236
- })
237
- },
238
- [companyId],
239
- )
240
-
241
- const displayedPeople = React.useMemo(
242
- () => (visiblePeople.length > 0 ? visiblePeople : people),
243
- [people, visiblePeople],
244
- )
245
- const totalLinkedPeople = listTotalCount > 0 ? listTotalCount : displayedPeople.length
246
- const decisionMakerNames = React.useMemo(
247
- () =>
248
- displayedPeople
249
- .filter((person) => starredIds.has(person.id))
250
- .map((person) => person.displayName),
251
- [displayedPeople, starredIds],
252
- )
253
-
254
201
  React.useEffect(() => {
255
202
  onActionChange?.(null)
256
203
  return () => onActionChange?.(null)
@@ -267,23 +214,27 @@ export function CompanyPeopleSection({
267
214
  onPeopleChange?.(people)
268
215
  }, [onPeopleChange, people])
269
216
 
270
- const loadVisiblePeople = React.useCallback(async () => {
271
- setListLoading(true)
272
- try {
217
+ const loadPage = React.useCallback(
218
+ async ({
219
+ page,
220
+ pageSize,
221
+ sort,
222
+ search,
223
+ }: {
224
+ page: number
225
+ pageSize: number
226
+ sort: LinkedPeopleSortMode
227
+ search: string
228
+ }): Promise<LinkedPeoplePage> => {
273
229
  const params = new URLSearchParams({
274
- page: String(listPage),
275
- pageSize: String(COMPANY_PEOPLE_PAGE_SIZE),
276
- sort: sortMode,
230
+ page: String(page),
231
+ pageSize: String(pageSize),
232
+ sort,
277
233
  })
278
- if (searchQuery.trim().length > 0) {
279
- params.set('search', searchQuery.trim())
234
+ if (search.length > 0) {
235
+ params.set('search', search)
280
236
  }
281
- const payload = await readApiResultOrThrow<{
282
- items?: CompanyPersonSummary[]
283
- page?: number
284
- total?: number
285
- totalPages?: number
286
- }>(
237
+ return readApiResultOrThrow<LinkedPeoplePage>(
287
238
  `/api/customers/companies/${encodeURIComponent(companyId)}/people?${params.toString()}`,
288
239
  undefined,
289
240
  {
@@ -293,34 +244,16 @@ export function CompanyPeopleSection({
293
244
  ),
294
245
  },
295
246
  )
296
- const nextTotalCount = typeof payload.total === 'number' ? payload.total : 0
297
- setVisiblePeople(Array.isArray(payload.items) ? payload.items : [])
298
- setListPage(typeof payload.page === 'number' ? payload.page : listPage)
299
- setListTotalCount((current) =>
300
- searchQuery.trim().length > 0 ? Math.max(current, nextTotalCount) : nextTotalCount,
301
- )
302
- setListTotalPages(typeof payload.totalPages === 'number' ? payload.totalPages : 1)
303
- } catch {
304
- setVisiblePeople([])
305
- if (searchQuery.trim().length === 0) {
306
- setListTotalCount(0)
307
- }
308
- setListTotalPages(1)
309
- } finally {
310
- setListLoading(false)
311
- }
312
- }, [companyId, listPage, searchQuery, sortMode, translate])
313
-
314
- React.useEffect(() => {
315
- void loadVisiblePeople()
316
- }, [loadVisiblePeople])
247
+ },
248
+ [companyId, translate],
249
+ )
317
250
 
318
251
  const reloadOnCompanyDetach = React.useCallback((event: AppEventPayload) => {
319
252
  const payload = event.payload as { companyEntityId?: string | null } | null | undefined
320
253
  if (payload && payload.companyEntityId === companyId) {
321
- void loadVisiblePeople()
254
+ requestRefresh()
322
255
  }
323
- }, [companyId, loadVisiblePeople])
256
+ }, [companyId, requestRefresh])
324
257
 
325
258
  useAppEvent('customers.person_company_link.deleted', reloadOnCompanyDetach, [reloadOnCompanyDetach])
326
259
  // Legacy profile-only assignments have no link row, so their detach broadcasts this sibling
@@ -328,10 +261,6 @@ export function CompanyPeopleSection({
328
261
  // of the same company keep listing a person who is already gone.
329
262
  useAppEvent('customers.person.company_assignment.detached', reloadOnCompanyDetach, [reloadOnCompanyDetach])
330
263
 
331
- React.useEffect(() => {
332
- setListPage(1)
333
- }, [searchQuery, sortMode])
334
-
335
264
  const applyPeopleChange = React.useCallback(
336
265
  (updater: (current: CompanyPersonSummary[]) => CompanyPersonSummary[]) => {
337
266
  setPeople((current) => {
@@ -351,11 +280,9 @@ export function CompanyPeopleSection({
351
280
  optionsById,
352
281
  }: {
353
282
  addedIds: string[]
354
- removedIds: string[]
355
283
  optionsById: Record<string, LinkEntityOption>
356
284
  }) => {
357
285
  if (!addedIds.length) return
358
- onLoadingChange?.(true)
359
286
  try {
360
287
  for (const personId of addedIds) {
361
288
  await runWriteMutation(
@@ -396,9 +323,7 @@ export function CompanyPeopleSection({
396
323
  .filter((entry): entry is CompanyPersonSummary => entry !== null)
397
324
  if (optimisticPeople.length > 0) {
398
325
  applyPeopleChange((current) => mergeCompanyPeople([...current, ...optimisticPeople]))
399
- setListTotalCount((current) => current + optimisticPeople.length)
400
326
  }
401
- await loadVisiblePeople()
402
327
  flash(
403
328
  addedIds.length === 1
404
329
  ? translate(
@@ -427,19 +352,9 @@ export function CompanyPeopleSection({
427
352
  )
428
353
  flash(message, 'error')
429
354
  throw err
430
- } finally {
431
- onLoadingChange?.(false)
432
355
  }
433
356
  },
434
- [
435
- applyPeopleChange,
436
- companyId,
437
- loadVisiblePeople,
438
- onDataRefresh,
439
- onLoadingChange,
440
- runWriteMutation,
441
- translate,
442
- ],
357
+ [applyPeopleChange, companyId, onDataRefresh, runWriteMutation, translate],
443
358
  )
444
359
 
445
360
  const personLinkAdapter = React.useMemo(
@@ -508,7 +423,7 @@ export function CompanyPeopleSection({
508
423
  // CreatePersonDialog already created and linked the person to this company
509
424
  // via the companyEntityId payload field. Refresh the on-page list and close
510
425
  // both the nested and outer dialogs so the user can see the new entry.
511
- void loadVisiblePeople()
426
+ requestRefresh()
512
427
  void onDataRefresh?.()
513
428
  setLinkDialogOpen(false)
514
429
  onCancel()
@@ -517,14 +432,11 @@ export function CompanyPeopleSection({
517
432
  ),
518
433
  },
519
434
  }),
520
- [companyId, companyName, loadVisiblePeople, onDataRefresh, runWriteMutation, translate],
435
+ [companyId, companyName, onDataRefresh, requestRefresh, runWriteMutation, translate],
521
436
  )
522
437
 
523
438
  const handleRemove = React.useCallback(
524
439
  async (personId: string) => {
525
- if (!personId || removingId) return
526
- setRemovingId(personId)
527
- onLoadingChange?.(true)
528
440
  try {
529
441
  await runWriteMutation(
530
442
  () =>
@@ -544,7 +456,6 @@ export function CompanyPeopleSection({
544
456
  companyId,
545
457
  },
546
458
  )
547
- await loadVisiblePeople()
548
459
  flash(
549
460
  translate(
550
461
  'customers.companies.detail.people.removeSuccess',
@@ -561,270 +472,65 @@ export function CompanyPeopleSection({
561
472
  'Failed to unlink person from company.',
562
473
  )
563
474
  flash(message, 'error')
564
- } finally {
565
- setRemovingId(null)
566
- onLoadingChange?.(false)
567
475
  }
568
476
  },
569
- [
570
- companyId,
571
- loadVisiblePeople,
572
- onLoadingChange,
573
- removingId,
574
- runWriteMutation,
575
- translate,
576
- ],
577
- )
578
-
579
- const linkAction = (
580
- <Button
581
- type="button"
582
- variant="outline"
583
- size="sm"
584
- onClick={() => setLinkDialogOpen(true)}
585
- >
586
- <Link2 className="mr-1.5 h-4 w-4" />
587
- {translate(
588
- 'customers.companies.detail.people.linkAction',
589
- 'Link existing person',
590
- )}
591
- </Button>
477
+ [companyId, runWriteMutation, translate],
592
478
  )
593
- const addPersonAction = (
594
- <Button type="button" size="sm" onClick={() => setCreateDialogOpen(true)}>
595
- <Plus className="mr-1.5 h-4 w-4" />
596
- {addActionLabel}
597
- </Button>
598
- )
599
-
600
- if (!listLoading && totalLinkedPeople === 0) {
601
- return (
602
- <>
603
- <EmptyState
604
- icon={<Users className="h-10 w-10 text-muted-foreground" />}
605
- title={emptyState.title}
606
- actionLabel={emptyState.actionLabel}
607
- onAction={() => setCreateDialogOpen(true)}
608
- >
609
- <p className="text-sm text-muted-foreground">{emptyLabel}</p>
610
- <div className="mt-4">{linkAction}</div>
611
- </EmptyState>
612
- <LinkEntityDialog
613
- open={linkDialogOpen}
614
- onOpenChange={setLinkDialogOpen}
615
- adapter={personLinkAdapter}
616
- initialSelectedIds={[]}
617
- onConfirm={handleLinkConfirm}
618
- runGuardedMutation={runWriteMutation}
619
- />
620
- <CreatePersonDialog
621
- open={createDialogOpen}
622
- onClose={() => setCreateDialogOpen(false)}
623
- companyId={companyId}
624
- companyName={companyName ?? companyId}
625
- runGuardedMutation={runWriteMutation}
626
- onPersonCreated={() => {
627
- setCreateDialogOpen(false)
628
- void loadVisiblePeople()
629
- void onDataRefresh?.()
630
- }}
631
- />
632
- </>
633
- )
634
- }
635
479
 
636
480
  return (
637
481
  <>
638
- <div className="space-y-4">
639
- <RolesSection
640
- entityType="company"
641
- entityId={companyId}
642
- entityName={companyName ?? null}
643
- />
644
-
645
- <section className="rounded-lg border bg-card px-4 py-4 sm:px-5">
646
- <div className="flex flex-col gap-4">
647
- <div className="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
648
- <div className="space-y-1">
649
- <div className="flex items-center gap-2">
650
- <h3 className="text-base font-semibold">
651
- {translate(
652
- 'customers.companies.detail.people.sectionTitle',
653
- 'People',
654
- )}
655
- </h3>
656
- <Badge
657
- variant="secondary"
658
- className="rounded-full px-2 py-0 text-xs font-semibold"
659
- >
660
- {totalLinkedPeople}
661
- </Badge>
662
- </div>
663
- <p className="text-xs text-muted-foreground">
664
- {translate(
665
- 'customers.companies.detail.people.sectionSubtitle',
666
- 'Employees and decision makers on the client side',
667
- )}
668
- </p>
669
- </div>
670
- <div className="flex flex-wrap items-center gap-2 xl:justify-end">
671
- {linkAction}
672
- {addPersonAction}
673
- </div>
674
- </div>
675
-
676
- {totalLinkedPeople > 0 ? (
677
- <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
678
- {filtersOpen ? (
679
- <div className="min-w-0 flex-1">
680
- <input
681
- type="text"
682
- value={searchQuery}
683
- onChange={(event) => setSearchQuery(event.target.value)}
684
- placeholder={translate(
685
- 'customers.companies.detail.people.searchPlaceholder',
686
- 'Search by name, role, email...',
687
- )}
688
- className="h-10 w-full rounded-md border bg-background px-3 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
689
- />
690
- </div>
691
- ) : null}
692
- <div className="flex flex-wrap items-center gap-2 lg:justify-end">
693
- <Button
694
- type="button"
695
- variant="outline"
696
- size="sm"
697
- onClick={() => setFiltersOpen((current) => !current)}
698
- className="h-10"
699
- >
700
- <Filter className="mr-1.5 h-4 w-4" />
701
- {translate(
702
- 'customers.companies.detail.people.filter',
703
- 'Filters',
704
- )}
705
- </Button>
706
- {filtersOpen ? (
707
- <select
708
- value={sortMode}
709
- onChange={(event) =>
710
- setSortMode(event.target.value as 'name-asc' | 'name-desc' | 'recent')
711
- }
712
- className="h-10 min-w-[11rem] rounded-md border bg-background px-3 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
713
- >
714
- <option value="name-asc">
715
- {translate(
716
- 'customers.companies.detail.people.sortNameAsc',
717
- 'Sort: Name A-Z',
718
- )}
719
- </option>
720
- <option value="name-desc">
721
- {translate(
722
- 'customers.companies.detail.people.sortNameDesc',
723
- 'Sort: Name Z-A',
724
- )}
725
- </option>
726
- <option value="recent">
727
- {translate(
728
- 'customers.companies.detail.people.sortRecent',
729
- 'Sort: Recently linked',
730
- )}
731
- </option>
732
- </select>
733
- ) : null}
734
- </div>
735
- </div>
736
- ) : null}
737
-
738
- {listLoading ? (
739
- <p className="py-6 text-center text-sm text-muted-foreground">
740
- {translate('customers.companies.detail.people.loading', 'Loading people…')}
741
- </p>
742
- ) : visiblePeople.length > 0 ? (
743
- <>
744
- <div
745
- className="grid items-start gap-4"
746
- style={{
747
- gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 19.5rem), 1fr))',
748
- }}
749
- >
750
- {visiblePeople.map((person) => (
751
- <PersonCard
752
- key={person.id}
753
- person={person}
754
- isStarred={starredIds.has(person.id)}
755
- onToggleStar={toggleStar}
756
- onUnlink={handleRemove}
757
- />
758
- ))}
759
- </div>
760
- {listTotalPages > 1 ? (
761
- <div className="flex items-center justify-between border-t border-border/60 pt-3 text-sm text-muted-foreground">
762
- <span>
763
- {translate(
764
- 'customers.companies.detail.people.pageSummary',
765
- 'Page {{page}} of {{total}}',
766
- {
767
- page: listPage,
768
- total: listTotalPages,
769
- },
770
- )}
771
- </span>
772
- <div className="flex items-center gap-2">
773
- <Button
774
- type="button"
775
- variant="outline"
776
- size="sm"
777
- onClick={() => setListPage((current) => Math.max(1, current - 1))}
778
- disabled={listPage <= 1}
779
- >
780
- {translate('customers.companies.detail.people.previous', 'Previous')}
781
- </Button>
782
- <Button
783
- type="button"
784
- variant="outline"
785
- size="sm"
786
- onClick={() =>
787
- setListPage((current) => Math.min(listTotalPages, current + 1))
788
- }
789
- disabled={listPage >= listTotalPages}
790
- >
791
- {translate('customers.companies.detail.people.next', 'Next')}
792
- </Button>
793
- </div>
794
- </div>
795
- ) : null}
796
- </>
797
- ) : totalLinkedPeople > 0 ? (
798
- <p className="py-6 text-center text-sm text-muted-foreground">
799
- {translate(
800
- 'customers.companies.detail.people.noSearchResults',
801
- 'No people match your search.',
802
- )}
803
- </p>
804
- ) : null}
805
- </div>
806
- </section>
807
-
808
- <DecisionMakersFooter
809
- names={decisionMakerNames}
810
- onSendInvitation={() => {
811
- const starredEmails = displayedPeople
812
- .filter((person) => starredIds.has(person.id) && person.primaryEmail)
813
- .map((person) => person.primaryEmail!)
814
- if (starredEmails.length > 0) {
815
- window.open(`mailto:${starredEmails.join(',')}`, '_blank')
816
- }
817
- }}
818
- />
819
- </div>
820
-
821
- <LinkEntityDialog
822
- open={linkDialogOpen}
823
- onOpenChange={setLinkDialogOpen}
824
- adapter={personLinkAdapter}
825
- initialSelectedIds={[]}
826
- onConfirm={handleLinkConfirm}
482
+ <LinkedPeopleSection
483
+ scopeId={companyId}
484
+ fallbackPeople={people}
485
+ loadPage={loadPage}
486
+ onUnlink={handleRemove}
487
+ linkAdapter={personLinkAdapter}
488
+ linkDialogOpen={linkDialogOpen}
489
+ onLinkDialogOpenChange={setLinkDialogOpen}
490
+ onLinkConfirm={handleLinkConfirm}
491
+ sectionTitle={translate('customers.companies.detail.people.sectionTitle', 'People')}
492
+ sectionSubtitle={translate(
493
+ 'customers.companies.detail.people.sectionSubtitle',
494
+ 'Employees and decision makers on the client side',
495
+ )}
496
+ searchPlaceholder={translate(
497
+ 'customers.companies.detail.people.searchPlaceholder',
498
+ 'Search by name, role, email...',
499
+ )}
500
+ linkActionLabel={translate(
501
+ 'customers.companies.detail.people.linkAction',
502
+ 'Link existing person',
503
+ )}
504
+ addActionLabel={addActionLabel}
505
+ onAddPerson={() => setCreateDialogOpen(true)}
506
+ emptyLabel={emptyLabel}
507
+ emptyState={emptyState}
508
+ refreshKey={refreshKey}
509
+ translator={translate}
827
510
  runGuardedMutation={runWriteMutation}
511
+ onLoadingChange={onLoadingChange}
512
+ header={
513
+ <RolesSection
514
+ entityType="company"
515
+ entityId={companyId}
516
+ entityName={companyName ?? null}
517
+ />
518
+ }
519
+ renderFooter={({ people: visible, starredIds }) => (
520
+ <DecisionMakersFooter
521
+ names={visible
522
+ .filter((person) => starredIds.has(person.id))
523
+ .map((person) => person.displayName)}
524
+ onSendInvitation={() => {
525
+ const starredEmails = visible
526
+ .filter((person) => starredIds.has(person.id) && person.primaryEmail)
527
+ .map((person) => person.primaryEmail!)
528
+ if (starredEmails.length > 0) {
529
+ window.open(`mailto:${starredEmails.join(',')}`, '_blank')
530
+ }
531
+ }}
532
+ />
533
+ )}
828
534
  />
829
535
 
830
536
  <CreatePersonDialog
@@ -835,7 +541,7 @@ export function CompanyPeopleSection({
835
541
  runGuardedMutation={runWriteMutation}
836
542
  onPersonCreated={() => {
837
543
  setCreateDialogOpen(false)
838
- void loadVisiblePeople()
544
+ requestRefresh()
839
545
  void onDataRefresh?.()
840
546
  }}
841
547
  />