@open-mercato/core 0.5.1-develop.2663.2c29774b5b → 0.5.1-develop.2681.c559bb2bc3

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 (687) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/action_log/index.js +8 -0
  3. package/dist/generated/entities/action_log/index.js.map +2 -2
  4. package/dist/generated/entities/customer_company_billing/index.js +23 -0
  5. package/dist/generated/entities/customer_company_billing/index.js.map +7 -0
  6. package/dist/generated/entities/customer_deal/index.js +8 -0
  7. package/dist/generated/entities/customer_deal/index.js.map +2 -2
  8. package/dist/generated/entities/customer_deal_stage_transition/index.js +31 -0
  9. package/dist/generated/entities/customer_deal_stage_transition/index.js.map +7 -0
  10. package/dist/generated/entities/customer_dictionary_kind_setting/index.js +21 -0
  11. package/dist/generated/entities/customer_dictionary_kind_setting/index.js.map +7 -0
  12. package/dist/generated/entities/customer_entity/index.js +8 -0
  13. package/dist/generated/entities/customer_entity/index.js.map +2 -2
  14. package/dist/generated/entities/customer_entity_role/index.js +23 -0
  15. package/dist/generated/entities/customer_entity_role/index.js.map +7 -0
  16. package/dist/generated/entities/customer_interaction/index.js +23 -1
  17. package/dist/generated/entities/customer_interaction/index.js.map +2 -2
  18. package/dist/generated/entities/customer_label/index.js +19 -0
  19. package/dist/generated/entities/customer_label/index.js.map +7 -0
  20. package/dist/generated/entities/customer_label_assignment/index.js +17 -0
  21. package/dist/generated/entities/customer_label_assignment/index.js.map +7 -0
  22. package/dist/generated/entities/customer_person_company_link/index.js +21 -0
  23. package/dist/generated/entities/customer_person_company_link/index.js.map +7 -0
  24. package/dist/generated/entities/customer_person_company_role/index.js +17 -0
  25. package/dist/generated/entities/customer_person_company_role/index.js.map +7 -0
  26. package/dist/generated/entities/dictionary_entry/index.js +4 -0
  27. package/dist/generated/entities/dictionary_entry/index.js.map +2 -2
  28. package/dist/generated/entities.ids.generated.js +9 -1
  29. package/dist/generated/entities.ids.generated.js.map +2 -2
  30. package/dist/generated/entity-fields-registry.js +116 -1
  31. package/dist/generated/entity-fields-registry.js.map +2 -2
  32. package/dist/modules/attachments/api/route.js +46 -8
  33. package/dist/modules/attachments/api/route.js.map +2 -2
  34. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js +208 -0
  35. package/dist/modules/audit_logs/api/audit-logs/actions/export/route.js.map +7 -0
  36. package/dist/modules/audit_logs/api/audit-logs/actions/route.js +52 -6
  37. package/dist/modules/audit_logs/api/audit-logs/actions/route.js.map +2 -2
  38. package/dist/modules/audit_logs/cli.js +62 -0
  39. package/dist/modules/audit_logs/cli.js.map +7 -0
  40. package/dist/modules/audit_logs/data/entities.js +21 -1
  41. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  42. package/dist/modules/audit_logs/data/validators.js +9 -1
  43. package/dist/modules/audit_logs/data/validators.js.map +2 -2
  44. package/dist/modules/audit_logs/lib/changeRows.js +34 -0
  45. package/dist/modules/audit_logs/lib/changeRows.js.map +7 -0
  46. package/dist/modules/audit_logs/lib/display-helpers.js +2 -20
  47. package/dist/modules/audit_logs/lib/display-helpers.js.map +3 -3
  48. package/dist/modules/audit_logs/lib/projections.js +58 -0
  49. package/dist/modules/audit_logs/lib/projections.js.map +7 -0
  50. package/dist/modules/audit_logs/migrations/Migration20260412160533.js +21 -0
  51. package/dist/modules/audit_logs/migrations/Migration20260412160533.js.map +7 -0
  52. package/dist/modules/audit_logs/services/actionLogService.js +313 -79
  53. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  54. package/dist/modules/customers/acl.js +3 -1
  55. package/dist/modules/customers/acl.js.map +2 -2
  56. package/dist/modules/customers/api/activities/route.js +4 -0
  57. package/dist/modules/customers/api/activities/route.js.map +2 -2
  58. package/dist/modules/customers/api/assignable-staff/route.js +208 -0
  59. package/dist/modules/customers/api/assignable-staff/route.js.map +7 -0
  60. package/dist/modules/customers/api/companies/[id]/people/route.js +205 -0
  61. package/dist/modules/customers/api/companies/[id]/people/route.js.map +7 -0
  62. package/dist/modules/customers/api/companies/[id]/roles/route.js +22 -0
  63. package/dist/modules/customers/api/companies/[id]/roles/route.js.map +7 -0
  64. package/dist/modules/customers/api/companies/[id]/route.js +374 -32
  65. package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
  66. package/dist/modules/customers/api/companies/route.js +82 -7
  67. package/dist/modules/customers/api/companies/route.js.map +2 -2
  68. package/dist/modules/customers/api/deals/[id]/companies/route.js +172 -0
  69. package/dist/modules/customers/api/deals/[id]/companies/route.js.map +7 -0
  70. package/dist/modules/customers/api/deals/[id]/people/route.js +156 -0
  71. package/dist/modules/customers/api/deals/[id]/people/route.js.map +7 -0
  72. package/dist/modules/customers/api/deals/[id]/route.js +459 -53
  73. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  74. package/dist/modules/customers/api/deals/[id]/stats/route.js +195 -0
  75. package/dist/modules/customers/api/deals/[id]/stats/route.js.map +7 -0
  76. package/dist/modules/customers/api/deals/route.js +20 -10
  77. package/dist/modules/customers/api/deals/route.js.map +3 -3
  78. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +105 -4
  79. package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
  80. package/dist/modules/customers/api/dictionaries/[kind]/route.js +118 -42
  81. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  82. package/dist/modules/customers/api/dictionaries/context.js +30 -6
  83. package/dist/modules/customers/api/dictionaries/context.js.map +2 -2
  84. package/dist/modules/customers/api/dictionaries/kind-settings/route.js +207 -0
  85. package/dist/modules/customers/api/dictionaries/kind-settings/route.js.map +7 -0
  86. package/dist/modules/customers/api/entity-roles-factory.js +471 -0
  87. package/dist/modules/customers/api/entity-roles-factory.js.map +7 -0
  88. package/dist/modules/customers/api/interactions/conflicts/route.js +158 -0
  89. package/dist/modules/customers/api/interactions/conflicts/route.js.map +7 -0
  90. package/dist/modules/customers/api/interactions/counts/route.js +92 -0
  91. package/dist/modules/customers/api/interactions/counts/route.js.map +7 -0
  92. package/dist/modules/customers/api/interactions/route.js +83 -4
  93. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  94. package/dist/modules/customers/api/labels/assign/route.js +189 -0
  95. package/dist/modules/customers/api/labels/assign/route.js.map +7 -0
  96. package/dist/modules/customers/api/labels/auth.js +17 -0
  97. package/dist/modules/customers/api/labels/auth.js.map +7 -0
  98. package/dist/modules/customers/api/labels/route.js +281 -0
  99. package/dist/modules/customers/api/labels/route.js.map +7 -0
  100. package/dist/modules/customers/api/labels/table-errors.js +38 -0
  101. package/dist/modules/customers/api/labels/table-errors.js.map +7 -0
  102. package/dist/modules/customers/api/labels/unassign/route.js +184 -0
  103. package/dist/modules/customers/api/labels/unassign/route.js.map +7 -0
  104. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js +292 -0
  105. package/dist/modules/customers/api/people/[id]/companies/[linkId]/route.js.map +7 -0
  106. package/dist/modules/customers/api/people/[id]/companies/context.js +66 -0
  107. package/dist/modules/customers/api/people/[id]/companies/context.js.map +7 -0
  108. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +334 -0
  109. package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +7 -0
  110. package/dist/modules/customers/api/people/[id]/companies/route.js +205 -0
  111. package/dist/modules/customers/api/people/[id]/companies/route.js.map +7 -0
  112. package/dist/modules/customers/api/people/[id]/roles/route.js +22 -0
  113. package/dist/modules/customers/api/people/[id]/roles/route.js.map +7 -0
  114. package/dist/modules/customers/api/people/[id]/route.js +134 -21
  115. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  116. package/dist/modules/customers/api/people/route.js +122 -23
  117. package/dist/modules/customers/api/people/route.js.map +2 -2
  118. package/dist/modules/customers/api/todos/route.js +4 -0
  119. package/dist/modules/customers/api/todos/route.js.map +2 -2
  120. package/dist/modules/customers/api/utils.js +22 -0
  121. package/dist/modules/customers/api/utils.js.map +2 -2
  122. package/dist/modules/customers/backend/config/customers/page.js +2 -6
  123. package/dist/modules/customers/backend/config/customers/page.js.map +2 -2
  124. package/dist/modules/customers/backend/customers/companies/page.js +37 -26
  125. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  126. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +265 -262
  127. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +3 -3
  128. package/dist/modules/customers/backend/customers/deals/[id]/hooks/formatters.js +23 -0
  129. package/dist/modules/customers/backend/customers/deals/[id]/hooks/formatters.js.map +7 -0
  130. package/dist/modules/customers/backend/customers/deals/[id]/hooks/types.js +1 -0
  131. package/dist/modules/customers/backend/customers/deals/[id]/hooks/types.js.map +7 -0
  132. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +43 -0
  133. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +7 -0
  134. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +264 -0
  135. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +7 -0
  136. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +88 -0
  137. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +7 -0
  138. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +41 -0
  139. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +7 -0
  140. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.js +66 -0
  141. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.js.map +7 -0
  142. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js +39 -0
  143. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js.map +7 -0
  144. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealMutationContext.js +49 -0
  145. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealMutationContext.js.map +7 -0
  146. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +43 -0
  147. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +7 -0
  148. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useScheduleDialog.js +28 -0
  149. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useScheduleDialog.js.map +7 -0
  150. package/dist/modules/customers/backend/customers/deals/[id]/page.js +556 -503
  151. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +3 -3
  152. package/dist/modules/customers/backend/customers/deals/page.js +66 -21
  153. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  154. package/dist/modules/customers/backend/customers/people/page.js +36 -28
  155. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  156. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +318 -203
  157. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +3 -3
  158. package/dist/modules/customers/cli.js +105 -13
  159. package/dist/modules/customers/cli.js.map +2 -2
  160. package/dist/modules/customers/commands/activities.js +6 -0
  161. package/dist/modules/customers/commands/activities.js.map +2 -2
  162. package/dist/modules/customers/commands/deals.js +315 -107
  163. package/dist/modules/customers/commands/deals.js.map +2 -2
  164. package/dist/modules/customers/commands/dictionaries.js +166 -32
  165. package/dist/modules/customers/commands/dictionaries.js.map +2 -2
  166. package/dist/modules/customers/commands/dictionaryKindSettings.js +208 -0
  167. package/dist/modules/customers/commands/dictionaryKindSettings.js.map +7 -0
  168. package/dist/modules/customers/commands/entity-roles.js +415 -0
  169. package/dist/modules/customers/commands/entity-roles.js.map +7 -0
  170. package/dist/modules/customers/commands/index.js +4 -0
  171. package/dist/modules/customers/commands/index.js.map +2 -2
  172. package/dist/modules/customers/commands/interactions.js +108 -21
  173. package/dist/modules/customers/commands/interactions.js.map +2 -2
  174. package/dist/modules/customers/commands/labels.js +539 -0
  175. package/dist/modules/customers/commands/labels.js.map +7 -0
  176. package/dist/modules/customers/commands/people.js +560 -463
  177. package/dist/modules/customers/commands/people.js.map +3 -3
  178. package/dist/modules/customers/commands/personCompanyLinks.js +568 -0
  179. package/dist/modules/customers/commands/personCompanyLinks.js.map +7 -0
  180. package/dist/modules/customers/commands/shared.js +12 -4
  181. package/dist/modules/customers/commands/shared.js.map +2 -2
  182. package/dist/modules/customers/commands/todos.js +10 -1
  183. package/dist/modules/customers/commands/todos.js.map +2 -2
  184. package/dist/modules/customers/components/AddressEditor.js +1 -1
  185. package/dist/modules/customers/components/AddressEditor.js.map +2 -2
  186. package/dist/modules/customers/components/CustomersConfigurationSections.js +31 -0
  187. package/dist/modules/customers/components/CustomersConfigurationSections.js.map +7 -0
  188. package/dist/modules/customers/components/DictionarySettings.js +37 -2
  189. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  190. package/dist/modules/customers/components/detail/ActiveDealCard.js +121 -0
  191. package/dist/modules/customers/components/detail/ActiveDealCard.js.map +7 -0
  192. package/dist/modules/customers/components/detail/ActivitiesSection.js +222 -331
  193. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +3 -3
  194. package/dist/modules/customers/components/detail/ActivityAiActions.js +36 -0
  195. package/dist/modules/customers/components/detail/ActivityAiActions.js.map +7 -0
  196. package/dist/modules/customers/components/detail/ActivityCard.js +126 -0
  197. package/dist/modules/customers/components/detail/ActivityCard.js.map +7 -0
  198. package/dist/modules/customers/components/detail/ActivityHistorySection.js +340 -0
  199. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +7 -0
  200. package/dist/modules/customers/components/detail/ActivityLogTab.js +56 -0
  201. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +7 -0
  202. package/dist/modules/customers/components/detail/ActivityTimeline.js +108 -0
  203. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +7 -0
  204. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +139 -0
  205. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +7 -0
  206. package/dist/modules/customers/components/detail/ActivityTypeSelector.js +42 -0
  207. package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +7 -0
  208. package/dist/modules/customers/components/detail/AiActionChips.js +38 -0
  209. package/dist/modules/customers/components/detail/AiActionChips.js.map +7 -0
  210. package/dist/modules/customers/components/detail/AssignRoleDialog.js +534 -0
  211. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +7 -0
  212. package/dist/modules/customers/components/detail/ChangelogEntryRow.js +79 -0
  213. package/dist/modules/customers/components/detail/ChangelogEntryRow.js.map +7 -0
  214. package/dist/modules/customers/components/detail/ChangelogFilters.js +176 -0
  215. package/dist/modules/customers/components/detail/ChangelogFilters.js.map +7 -0
  216. package/dist/modules/customers/components/detail/ChangelogKpiCards.js +88 -0
  217. package/dist/modules/customers/components/detail/ChangelogKpiCards.js.map +7 -0
  218. package/dist/modules/customers/components/detail/ChangelogTab.js +470 -0
  219. package/dist/modules/customers/components/detail/ChangelogTab.js.map +7 -0
  220. package/dist/modules/customers/components/detail/ComingSoonPlaceholder.js +16 -0
  221. package/dist/modules/customers/components/detail/ComingSoonPlaceholder.js.map +7 -0
  222. package/dist/modules/customers/components/detail/CompanyCard.js +283 -0
  223. package/dist/modules/customers/components/detail/CompanyCard.js.map +7 -0
  224. package/dist/modules/customers/components/detail/CompanyDashboardTab.js +133 -0
  225. package/dist/modules/customers/components/detail/CompanyDashboardTab.js.map +7 -0
  226. package/dist/modules/customers/components/detail/CompanyDetailHeader.js +191 -0
  227. package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +7 -0
  228. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +123 -0
  229. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +7 -0
  230. package/dist/modules/customers/components/detail/CompanyKpiBar.js +174 -0
  231. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +7 -0
  232. package/dist/modules/customers/components/detail/CompanyPeopleSection.js +514 -230
  233. package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
  234. package/dist/modules/customers/components/detail/CompanyTagsDialog.js +22 -0
  235. package/dist/modules/customers/components/detail/CompanyTagsDialog.js.map +7 -0
  236. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +159 -0
  237. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +7 -0
  238. package/dist/modules/customers/components/detail/CreatePersonDialog.js +135 -0
  239. package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +7 -0
  240. package/dist/modules/customers/components/detail/DealClosureActionBar.js +59 -0
  241. package/dist/modules/customers/components/detail/DealClosureActionBar.js.map +7 -0
  242. package/dist/modules/customers/components/detail/DealDetailHeader.js +237 -0
  243. package/dist/modules/customers/components/detail/DealDetailHeader.js.map +7 -0
  244. package/dist/modules/customers/components/detail/DealDetailTabs.js +109 -0
  245. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +7 -0
  246. package/dist/modules/customers/components/detail/DealForm.js +219 -92
  247. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  248. package/dist/modules/customers/components/detail/DealLinkedEntitiesTab.js +295 -0
  249. package/dist/modules/customers/components/detail/DealLinkedEntitiesTab.js.map +7 -0
  250. package/dist/modules/customers/components/detail/DealLostSummaryDialog.js +107 -0
  251. package/dist/modules/customers/components/detail/DealLostSummaryDialog.js.map +7 -0
  252. package/dist/modules/customers/components/detail/DealWonPopup.js +113 -0
  253. package/dist/modules/customers/components/detail/DealWonPopup.js.map +7 -0
  254. package/dist/modules/customers/components/detail/DealsSection.js +206 -193
  255. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  256. package/dist/modules/customers/components/detail/DecisionMakersFooter.js +39 -0
  257. package/dist/modules/customers/components/detail/DecisionMakersFooter.js.map +7 -0
  258. package/dist/modules/customers/components/detail/EntityTagsDialog.js +1096 -0
  259. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +7 -0
  260. package/dist/modules/customers/components/detail/InlineActivityComposer.js +197 -0
  261. package/dist/modules/customers/components/detail/InlineActivityComposer.js.map +7 -0
  262. package/dist/modules/customers/components/detail/ManageTagsDialog.js +1091 -0
  263. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +7 -0
  264. package/dist/modules/customers/components/detail/MiniWeekCalendar.js +272 -0
  265. package/dist/modules/customers/components/detail/MiniWeekCalendar.js.map +7 -0
  266. package/dist/modules/customers/components/detail/MobilePersonDetail.js +106 -0
  267. package/dist/modules/customers/components/detail/MobilePersonDetail.js.map +7 -0
  268. package/dist/modules/customers/components/detail/NextStepCard.js +72 -0
  269. package/dist/modules/customers/components/detail/NextStepCard.js.map +7 -0
  270. package/dist/modules/customers/components/detail/PersonCard.js +192 -0
  271. package/dist/modules/customers/components/detail/PersonCard.js.map +7 -0
  272. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +345 -0
  273. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +7 -0
  274. package/dist/modules/customers/components/detail/PersonDetailHeader.js +220 -0
  275. package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +7 -0
  276. package/dist/modules/customers/components/detail/PersonDetailTabs.js +122 -0
  277. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +7 -0
  278. package/dist/modules/customers/components/detail/PersonTagsDialog.js +24 -0
  279. package/dist/modules/customers/components/detail/PersonTagsDialog.js.map +7 -0
  280. package/dist/modules/customers/components/detail/PipelineStepper.js +191 -0
  281. package/dist/modules/customers/components/detail/PipelineStepper.js.map +7 -0
  282. package/dist/modules/customers/components/detail/PlannedActivitiesSection.js +222 -0
  283. package/dist/modules/customers/components/detail/PlannedActivitiesSection.js.map +7 -0
  284. package/dist/modules/customers/components/detail/RelationshipHealthCard.js +49 -0
  285. package/dist/modules/customers/components/detail/RelationshipHealthCard.js.map +7 -0
  286. package/dist/modules/customers/components/detail/RoleAssignmentRow.js +189 -0
  287. package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +7 -0
  288. package/dist/modules/customers/components/detail/RolesSection.js +234 -0
  289. package/dist/modules/customers/components/detail/RolesSection.js.map +7 -0
  290. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +410 -0
  291. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +7 -0
  292. package/dist/modules/customers/components/detail/aiActionCatalog.js +41 -0
  293. package/dist/modules/customers/components/detail/aiActionCatalog.js.map +7 -0
  294. package/dist/modules/customers/components/detail/assignableStaff.js +48 -0
  295. package/dist/modules/customers/components/detail/assignableStaff.js.map +7 -0
  296. package/dist/modules/customers/components/detail/dashboard/ActiveDealWidget.js +48 -0
  297. package/dist/modules/customers/components/detail/dashboard/ActiveDealWidget.js.map +7 -0
  298. package/dist/modules/customers/components/detail/dashboard/OpenTasksWidget.js +86 -0
  299. package/dist/modules/customers/components/detail/dashboard/OpenTasksWidget.js.map +7 -0
  300. package/dist/modules/customers/components/detail/dashboard/RecentActivityWidget.js +53 -0
  301. package/dist/modules/customers/components/detail/dashboard/RecentActivityWidget.js.map +7 -0
  302. package/dist/modules/customers/components/detail/dashboard/RelationshipHealthWidget.js +30 -0
  303. package/dist/modules/customers/components/detail/dashboard/RelationshipHealthWidget.js.map +7 -0
  304. package/dist/modules/customers/components/detail/dashboard/UpcomingMeetingsWidget.js +43 -0
  305. package/dist/modules/customers/components/detail/dashboard/UpcomingMeetingsWidget.js.map +7 -0
  306. package/dist/modules/customers/components/detail/dashboard/helpers.js +71 -0
  307. package/dist/modules/customers/components/detail/dashboard/helpers.js.map +7 -0
  308. package/dist/modules/customers/components/detail/healthScoreUtils.js +69 -0
  309. package/dist/modules/customers/components/detail/healthScoreUtils.js.map +7 -0
  310. package/dist/modules/customers/components/detail/hooks/useCurrencyDictionary.js +5 -5
  311. package/dist/modules/customers/components/detail/hooks/useCurrencyDictionary.js.map +2 -2
  312. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +9 -8
  313. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +3 -3
  314. package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js +65 -0
  315. package/dist/modules/customers/components/detail/hooks/useInteractionMutations.js.map +7 -0
  316. package/dist/modules/customers/components/detail/notesAdapter.js +70 -30
  317. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  318. package/dist/modules/customers/components/detail/pipelineStageUtils.js +26 -0
  319. package/dist/modules/customers/components/detail/pipelineStageUtils.js.map +7 -0
  320. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +144 -0
  321. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +7 -0
  322. package/dist/modules/customers/components/detail/schedule/FooterFields.js +60 -0
  323. package/dist/modules/customers/components/detail/schedule/FooterFields.js.map +7 -0
  324. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +216 -0
  325. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +7 -0
  326. package/dist/modules/customers/components/detail/schedule/LocationField.js +34 -0
  327. package/dist/modules/customers/components/detail/schedule/LocationField.js.map +7 -0
  328. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +226 -0
  329. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +7 -0
  330. package/dist/modules/customers/components/detail/schedule/fieldConfig.js +69 -0
  331. package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +7 -0
  332. package/dist/modules/customers/components/detail/schedule/index.js +21 -0
  333. package/dist/modules/customers/components/detail/schedule/index.js.map +7 -0
  334. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +172 -0
  335. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +7 -0
  336. package/dist/modules/customers/components/detail/utils.js +23 -0
  337. package/dist/modules/customers/components/detail/utils.js.map +2 -2
  338. package/dist/modules/customers/components/formConfig.js +144 -22
  339. package/dist/modules/customers/components/formConfig.js.map +2 -2
  340. package/dist/modules/customers/components/linking/LinkEntityDialog.js +661 -0
  341. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +7 -0
  342. package/dist/modules/customers/components/linking/adapters/companyAdapter.js +252 -0
  343. package/dist/modules/customers/components/linking/adapters/companyAdapter.js.map +7 -0
  344. package/dist/modules/customers/components/linking/adapters/dealAdapter.js +384 -0
  345. package/dist/modules/customers/components/linking/adapters/dealAdapter.js.map +7 -0
  346. package/dist/modules/customers/components/linking/adapters/personAdapter.js +324 -0
  347. package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +7 -0
  348. package/dist/modules/customers/components/list/CollectionPreviewCell.js +53 -0
  349. package/dist/modules/customers/components/list/CollectionPreviewCell.js.map +7 -0
  350. package/dist/modules/customers/data/entities.js +407 -1
  351. package/dist/modules/customers/data/entities.js.map +2 -2
  352. package/dist/modules/customers/data/validators.js +139 -21
  353. package/dist/modules/customers/data/validators.js.map +2 -2
  354. package/dist/modules/customers/events.js +19 -1
  355. package/dist/modules/customers/events.js.map +2 -2
  356. package/dist/modules/customers/lib/customerRoleTypes.js +19 -0
  357. package/dist/modules/customers/lib/customerRoleTypes.js.map +7 -0
  358. package/dist/modules/customers/lib/dealClosureNotification.js +39 -0
  359. package/dist/modules/customers/lib/dealClosureNotification.js.map +7 -0
  360. package/dist/modules/customers/lib/dealStageTransitionTable.js +29 -0
  361. package/dist/modules/customers/lib/dealStageTransitionTable.js.map +7 -0
  362. package/dist/modules/customers/lib/dictionaries.js +25 -0
  363. package/dist/modules/customers/lib/dictionaries.js.map +2 -2
  364. package/dist/modules/customers/lib/interactionReadModel.js +10 -0
  365. package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
  366. package/dist/modules/customers/lib/personCompanies.js +235 -0
  367. package/dist/modules/customers/lib/personCompanies.js.map +7 -0
  368. package/dist/modules/customers/lib/personCompanyLinkTable.js +42 -0
  369. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +7 -0
  370. package/dist/modules/customers/lib/roleTypeUsage.js +104 -0
  371. package/dist/modules/customers/lib/roleTypeUsage.js.map +7 -0
  372. package/dist/modules/customers/migrations/Migration20260406214502.js +18 -0
  373. package/dist/modules/customers/migrations/Migration20260406214502.js.map +7 -0
  374. package/dist/modules/customers/migrations/Migration20260408135736.js +17 -0
  375. package/dist/modules/customers/migrations/Migration20260408135736.js.map +7 -0
  376. package/dist/modules/customers/migrations/Migration20260408225345.js +21 -0
  377. package/dist/modules/customers/migrations/Migration20260408225345.js.map +7 -0
  378. package/dist/modules/customers/migrations/Migration20260411075533.js +27 -0
  379. package/dist/modules/customers/migrations/Migration20260411075533.js.map +7 -0
  380. package/dist/modules/customers/migrations/Migration20260411103551.js +13 -0
  381. package/dist/modules/customers/migrations/Migration20260411103551.js.map +7 -0
  382. package/dist/modules/customers/migrations/Migration20260411130944.js +26 -0
  383. package/dist/modules/customers/migrations/Migration20260411130944.js.map +7 -0
  384. package/dist/modules/customers/migrations/Migration20260415095203.js +13 -0
  385. package/dist/modules/customers/migrations/Migration20260415095203.js.map +7 -0
  386. package/dist/modules/customers/migrations/Migration20260415135056.js +20 -0
  387. package/dist/modules/customers/migrations/Migration20260415135056.js.map +7 -0
  388. package/dist/modules/customers/migrations/Migration20260417140000.js +15 -0
  389. package/dist/modules/customers/migrations/Migration20260417140000.js.map +7 -0
  390. package/dist/modules/customers/migrations/Migration20260417160000.js +17 -0
  391. package/dist/modules/customers/migrations/Migration20260417160000.js.map +7 -0
  392. package/dist/modules/customers/migrations/Migration20260417235407.js +13 -0
  393. package/dist/modules/customers/migrations/Migration20260417235407.js.map +7 -0
  394. package/dist/modules/customers/setup.js +16 -1
  395. package/dist/modules/customers/setup.js.map +2 -2
  396. package/dist/modules/customers/subscribers/deal-closure-notification.js +16 -0
  397. package/dist/modules/customers/subscribers/deal-closure-notification.js.map +7 -0
  398. package/dist/modules/customers/subscribers/deal-lost-notification.js +16 -0
  399. package/dist/modules/customers/subscribers/deal-lost-notification.js.map +7 -0
  400. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +2 -0
  401. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  402. package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js +154 -0
  403. package/dist/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.js.map +7 -0
  404. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +6 -2
  405. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  406. package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js +154 -0
  407. package/dist/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.js.map +7 -0
  408. package/dist/modules/dictionaries/api/context.js +8 -1
  409. package/dist/modules/dictionaries/api/context.js.map +2 -2
  410. package/dist/modules/dictionaries/api/openapi.js +18 -1
  411. package/dist/modules/dictionaries/api/openapi.js.map +2 -2
  412. package/dist/modules/dictionaries/commands/entry-operations.js +388 -0
  413. package/dist/modules/dictionaries/commands/entry-operations.js.map +7 -0
  414. package/dist/modules/dictionaries/commands/factory.js +24 -3
  415. package/dist/modules/dictionaries/commands/factory.js.map +2 -2
  416. package/dist/modules/dictionaries/commands/index.js +1 -0
  417. package/dist/modules/dictionaries/commands/index.js.map +2 -2
  418. package/dist/modules/dictionaries/components/DictionaryTable.js +6 -3
  419. package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
  420. package/dist/modules/dictionaries/data/entities.js +11 -1
  421. package/dist/modules/dictionaries/data/entities.js.map +2 -2
  422. package/dist/modules/dictionaries/data/validators.js +28 -2
  423. package/dist/modules/dictionaries/data/validators.js.map +2 -2
  424. package/dist/modules/dictionaries/events.js +18 -0
  425. package/dist/modules/dictionaries/events.js.map +7 -0
  426. package/dist/modules/dictionaries/lib/clientEntries.js +43 -0
  427. package/dist/modules/dictionaries/lib/clientEntries.js.map +7 -0
  428. package/dist/modules/dictionaries/migrations/Migration20260410171544.js +45 -0
  429. package/dist/modules/dictionaries/migrations/Migration20260410171544.js.map +7 -0
  430. package/dist/modules/inbox_ops/api/proposals/[id]/route.js +4 -1
  431. package/dist/modules/inbox_ops/api/proposals/[id]/route.js.map +2 -2
  432. package/dist/modules/query_index/lib/engine.js +1 -1
  433. package/dist/modules/query_index/lib/engine.js.map +2 -2
  434. package/dist/modules/sales/components/documents/AddressesSection.js +82 -42
  435. package/dist/modules/sales/components/documents/AddressesSection.js.map +2 -2
  436. package/dist/modules/sales/lib/dictionaries.js +16 -0
  437. package/dist/modules/sales/lib/dictionaries.js.map +2 -2
  438. package/dist/modules/sales/widgets/injection-table.js +5 -1
  439. package/dist/modules/sales/widgets/injection-table.js.map +2 -2
  440. package/generated/entities/action_log/index.ts +4 -0
  441. package/generated/entities/customer_company_billing/index.ts +10 -0
  442. package/generated/entities/customer_deal/index.ts +4 -0
  443. package/generated/entities/customer_deal_stage_transition/index.ts +14 -0
  444. package/generated/entities/customer_dictionary_kind_setting/index.ts +9 -0
  445. package/generated/entities/customer_entity/index.ts +4 -0
  446. package/generated/entities/customer_entity_role/index.ts +10 -0
  447. package/generated/entities/customer_interaction/index.ts +11 -0
  448. package/generated/entities/customer_label/index.ts +8 -0
  449. package/generated/entities/customer_label_assignment/index.ts +7 -0
  450. package/generated/entities/customer_person_company_link/index.ts +9 -0
  451. package/generated/entities/customer_person_company_role/index.ts +7 -0
  452. package/generated/entities/dictionary_entry/index.ts +2 -0
  453. package/generated/entities.ids.generated.ts +9 -1
  454. package/generated/entity-fields-registry.ts +116 -1
  455. package/package.json +3 -3
  456. package/src/modules/attachments/api/route.ts +48 -6
  457. package/src/modules/attachments/i18n/de.json +4 -0
  458. package/src/modules/attachments/i18n/en.json +4 -0
  459. package/src/modules/attachments/i18n/es.json +4 -0
  460. package/src/modules/attachments/i18n/pl.json +4 -0
  461. package/src/modules/audit_logs/api/audit-logs/actions/export/route.ts +260 -0
  462. package/src/modules/audit_logs/api/audit-logs/actions/route.ts +81 -6
  463. package/src/modules/audit_logs/cli.ts +79 -0
  464. package/src/modules/audit_logs/data/entities.ts +17 -0
  465. package/src/modules/audit_logs/data/validators.ts +9 -1
  466. package/src/modules/audit_logs/lib/changeRows.ts +47 -0
  467. package/src/modules/audit_logs/lib/display-helpers.tsx +4 -30
  468. package/src/modules/audit_logs/lib/projections.ts +110 -0
  469. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +325 -2
  470. package/src/modules/audit_logs/migrations/Migration20260412160533.ts +21 -0
  471. package/src/modules/audit_logs/services/actionLogService.ts +455 -85
  472. package/src/modules/catalog/i18n/de.json +1 -0
  473. package/src/modules/catalog/i18n/en.json +1 -0
  474. package/src/modules/catalog/i18n/es.json +1 -0
  475. package/src/modules/catalog/i18n/pl.json +1 -0
  476. package/src/modules/customer_accounts/i18n/de.json +2 -0
  477. package/src/modules/customer_accounts/i18n/en.json +2 -0
  478. package/src/modules/customer_accounts/i18n/es.json +2 -0
  479. package/src/modules/customer_accounts/i18n/pl.json +2 -0
  480. package/src/modules/customers/acl.ts +2 -0
  481. package/src/modules/customers/api/activities/route.ts +4 -0
  482. package/src/modules/customers/api/assignable-staff/route.ts +250 -0
  483. package/src/modules/customers/api/companies/[id]/people/route.ts +244 -0
  484. package/src/modules/customers/api/companies/[id]/roles/route.ts +15 -0
  485. package/src/modules/customers/api/companies/[id]/route.ts +458 -40
  486. package/src/modules/customers/api/companies/route.ts +93 -15
  487. package/src/modules/customers/api/deals/[id]/companies/route.ts +203 -0
  488. package/src/modules/customers/api/deals/[id]/people/route.ts +182 -0
  489. package/src/modules/customers/api/deals/[id]/route.ts +554 -57
  490. package/src/modules/customers/api/deals/[id]/stats/route.ts +221 -0
  491. package/src/modules/customers/api/deals/route.ts +35 -46
  492. package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +105 -3
  493. package/src/modules/customers/api/dictionaries/[kind]/route.ts +143 -44
  494. package/src/modules/customers/api/dictionaries/context.ts +45 -16
  495. package/src/modules/customers/api/dictionaries/kind-settings/route.ts +232 -0
  496. package/src/modules/customers/api/entity-roles-factory.ts +520 -0
  497. package/src/modules/customers/api/interactions/conflicts/route.ts +196 -0
  498. package/src/modules/customers/api/interactions/counts/route.ts +112 -0
  499. package/src/modules/customers/api/interactions/route.ts +95 -2
  500. package/src/modules/customers/api/labels/assign/route.ts +202 -0
  501. package/src/modules/customers/api/labels/auth.ts +19 -0
  502. package/src/modules/customers/api/labels/route.ts +310 -0
  503. package/src/modules/customers/api/labels/table-errors.ts +36 -0
  504. package/src/modules/customers/api/labels/unassign/route.ts +197 -0
  505. package/src/modules/customers/api/people/[id]/companies/[linkId]/route.ts +331 -0
  506. package/src/modules/customers/api/people/[id]/companies/context.ts +70 -0
  507. package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +384 -0
  508. package/src/modules/customers/api/people/[id]/companies/route.ts +215 -0
  509. package/src/modules/customers/api/people/[id]/roles/route.ts +15 -0
  510. package/src/modules/customers/api/people/[id]/route.ts +153 -26
  511. package/src/modules/customers/api/people/route.ts +134 -31
  512. package/src/modules/customers/api/todos/route.ts +4 -0
  513. package/src/modules/customers/api/utils.ts +36 -0
  514. package/src/modules/customers/backend/config/customers/page.tsx +2 -6
  515. package/src/modules/customers/backend/customers/companies/page.tsx +36 -26
  516. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +277 -262
  517. package/src/modules/customers/backend/customers/deals/[id]/hooks/formatters.ts +19 -0
  518. package/src/modules/customers/backend/customers/deals/[id]/hooks/types.ts +104 -0
  519. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +60 -0
  520. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +362 -0
  521. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +113 -0
  522. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +52 -0
  523. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealFormHandlers.ts +86 -0
  524. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.tsx +60 -0
  525. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealMutationContext.ts +76 -0
  526. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +56 -0
  527. package/src/modules/customers/backend/customers/deals/[id]/hooks/useScheduleDialog.ts +38 -0
  528. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +587 -624
  529. package/src/modules/customers/backend/customers/deals/page.tsx +71 -28
  530. package/src/modules/customers/backend/customers/people/page.tsx +35 -29
  531. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +343 -209
  532. package/src/modules/customers/cli.ts +107 -12
  533. package/src/modules/customers/commands/activities.ts +13 -0
  534. package/src/modules/customers/commands/deals.ts +386 -114
  535. package/src/modules/customers/commands/dictionaries.ts +175 -32
  536. package/src/modules/customers/commands/dictionaryKindSettings.ts +268 -0
  537. package/src/modules/customers/commands/entity-roles.ts +494 -0
  538. package/src/modules/customers/commands/index.ts +4 -0
  539. package/src/modules/customers/commands/interactions.ts +125 -21
  540. package/src/modules/customers/commands/labels.ts +626 -0
  541. package/src/modules/customers/commands/people.ts +373 -259
  542. package/src/modules/customers/commands/personCompanyLinks.ts +654 -0
  543. package/src/modules/customers/commands/shared.ts +16 -15
  544. package/src/modules/customers/commands/todos.ts +17 -1
  545. package/src/modules/customers/components/AddressEditor.tsx +1 -1
  546. package/src/modules/customers/components/CustomersConfigurationSections.tsx +36 -0
  547. package/src/modules/customers/components/DictionarySettings.tsx +43 -2
  548. package/src/modules/customers/components/detail/ActiveDealCard.tsx +175 -0
  549. package/src/modules/customers/components/detail/ActivitiesSection.tsx +267 -361
  550. package/src/modules/customers/components/detail/ActivityAiActions.tsx +49 -0
  551. package/src/modules/customers/components/detail/ActivityCard.tsx +154 -0
  552. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +412 -0
  553. package/src/modules/customers/components/detail/ActivityLogTab.tsx +67 -0
  554. package/src/modules/customers/components/detail/ActivityTimeline.tsx +158 -0
  555. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +163 -0
  556. package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +53 -0
  557. package/src/modules/customers/components/detail/AiActionChips.tsx +48 -0
  558. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +672 -0
  559. package/src/modules/customers/components/detail/ChangelogEntryRow.tsx +132 -0
  560. package/src/modules/customers/components/detail/ChangelogFilters.tsx +193 -0
  561. package/src/modules/customers/components/detail/ChangelogKpiCards.tsx +107 -0
  562. package/src/modules/customers/components/detail/ChangelogTab.tsx +629 -0
  563. package/src/modules/customers/components/detail/ComingSoonPlaceholder.tsx +21 -0
  564. package/src/modules/customers/components/detail/CompanyCard.tsx +419 -0
  565. package/src/modules/customers/components/detail/CompanyDashboardTab.tsx +161 -0
  566. package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +243 -0
  567. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +172 -0
  568. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +206 -0
  569. package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +582 -288
  570. package/src/modules/customers/components/detail/CompanyTagsDialog.tsx +23 -0
  571. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +210 -0
  572. package/src/modules/customers/components/detail/CreatePersonDialog.tsx +178 -0
  573. package/src/modules/customers/components/detail/DealClosureActionBar.tsx +63 -0
  574. package/src/modules/customers/components/detail/DealDetailHeader.tsx +335 -0
  575. package/src/modules/customers/components/detail/DealDetailTabs.tsx +154 -0
  576. package/src/modules/customers/components/detail/DealForm.tsx +253 -101
  577. package/src/modules/customers/components/detail/DealLinkedEntitiesTab.tsx +349 -0
  578. package/src/modules/customers/components/detail/DealLostSummaryDialog.tsx +156 -0
  579. package/src/modules/customers/components/detail/DealWonPopup.tsx +164 -0
  580. package/src/modules/customers/components/detail/DealsSection.tsx +276 -221
  581. package/src/modules/customers/components/detail/DecisionMakersFooter.tsx +56 -0
  582. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +1372 -0
  583. package/src/modules/customers/components/detail/InlineActivityComposer.tsx +239 -0
  584. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1331 -0
  585. package/src/modules/customers/components/detail/MiniWeekCalendar.tsx +338 -0
  586. package/src/modules/customers/components/detail/MobilePersonDetail.tsx +124 -0
  587. package/src/modules/customers/components/detail/NextStepCard.tsx +104 -0
  588. package/src/modules/customers/components/detail/PersonCard.tsx +238 -0
  589. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +426 -0
  590. package/src/modules/customers/components/detail/PersonDetailHeader.tsx +294 -0
  591. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +172 -0
  592. package/src/modules/customers/components/detail/PersonTagsDialog.tsx +26 -0
  593. package/src/modules/customers/components/detail/PipelineStepper.tsx +245 -0
  594. package/src/modules/customers/components/detail/PlannedActivitiesSection.tsx +255 -0
  595. package/src/modules/customers/components/detail/RelationshipHealthCard.tsx +63 -0
  596. package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +248 -0
  597. package/src/modules/customers/components/detail/RolesSection.tsx +311 -0
  598. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +481 -0
  599. package/src/modules/customers/components/detail/aiActionCatalog.ts +77 -0
  600. package/src/modules/customers/components/detail/assignableStaff.ts +124 -0
  601. package/src/modules/customers/components/detail/dashboard/ActiveDealWidget.tsx +63 -0
  602. package/src/modules/customers/components/detail/dashboard/OpenTasksWidget.tsx +114 -0
  603. package/src/modules/customers/components/detail/dashboard/RecentActivityWidget.tsx +69 -0
  604. package/src/modules/customers/components/detail/dashboard/RelationshipHealthWidget.tsx +40 -0
  605. package/src/modules/customers/components/detail/dashboard/UpcomingMeetingsWidget.tsx +64 -0
  606. package/src/modules/customers/components/detail/dashboard/helpers.ts +78 -0
  607. package/src/modules/customers/components/detail/healthScoreUtils.ts +91 -0
  608. package/src/modules/customers/components/detail/hooks/useCurrencyDictionary.ts +8 -8
  609. package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +10 -6
  610. package/src/modules/customers/components/detail/hooks/useInteractionMutations.ts +91 -0
  611. package/src/modules/customers/components/detail/notesAdapter.ts +91 -30
  612. package/src/modules/customers/components/detail/pipelineStageUtils.ts +29 -0
  613. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +187 -0
  614. package/src/modules/customers/components/detail/schedule/FooterFields.tsx +79 -0
  615. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +277 -0
  616. package/src/modules/customers/components/detail/schedule/LocationField.tsx +42 -0
  617. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +255 -0
  618. package/src/modules/customers/components/detail/schedule/fieldConfig.ts +70 -0
  619. package/src/modules/customers/components/detail/schedule/index.ts +9 -0
  620. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +221 -0
  621. package/src/modules/customers/components/detail/types.ts +16 -0
  622. package/src/modules/customers/components/detail/utils.ts +25 -0
  623. package/src/modules/customers/components/formConfig.tsx +223 -28
  624. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +920 -0
  625. package/src/modules/customers/components/linking/adapters/companyAdapter.tsx +398 -0
  626. package/src/modules/customers/components/linking/adapters/dealAdapter.tsx +578 -0
  627. package/src/modules/customers/components/linking/adapters/personAdapter.tsx +512 -0
  628. package/src/modules/customers/components/list/CollectionPreviewCell.tsx +66 -0
  629. package/src/modules/customers/data/entities.ts +353 -1
  630. package/src/modules/customers/data/validators.ts +170 -19
  631. package/src/modules/customers/events.ts +22 -0
  632. package/src/modules/customers/i18n/de.json +841 -2
  633. package/src/modules/customers/i18n/en.json +841 -2
  634. package/src/modules/customers/i18n/es.json +840 -1
  635. package/src/modules/customers/i18n/pl.json +841 -2
  636. package/src/modules/customers/lib/customerRoleTypes.ts +24 -0
  637. package/src/modules/customers/lib/dealClosureNotification.ts +64 -0
  638. package/src/modules/customers/lib/dealStageTransitionTable.ts +32 -0
  639. package/src/modules/customers/lib/dictionaries.ts +26 -10
  640. package/src/modules/customers/lib/interactionReadModel.ts +10 -0
  641. package/src/modules/customers/lib/personCompanies.ts +317 -0
  642. package/src/modules/customers/lib/personCompanyLinkTable.ts +58 -0
  643. package/src/modules/customers/lib/roleTypeUsage.ts +146 -0
  644. package/src/modules/customers/migrations/.snapshot-open-mercato.json +2747 -798
  645. package/src/modules/customers/migrations/Migration20260406214502.ts +19 -0
  646. package/src/modules/customers/migrations/Migration20260408135736.ts +15 -0
  647. package/src/modules/customers/migrations/Migration20260408225345.ts +23 -0
  648. package/src/modules/customers/migrations/Migration20260411075533.ts +30 -0
  649. package/src/modules/customers/migrations/Migration20260411103551.ts +13 -0
  650. package/src/modules/customers/migrations/Migration20260411130944.ts +30 -0
  651. package/src/modules/customers/migrations/Migration20260415095203.ts +13 -0
  652. package/src/modules/customers/migrations/Migration20260415135056.ts +22 -0
  653. package/src/modules/customers/migrations/Migration20260417140000.ts +15 -0
  654. package/src/modules/customers/migrations/Migration20260417160000.ts +17 -0
  655. package/src/modules/customers/migrations/Migration20260417235407.ts +13 -0
  656. package/src/modules/customers/setup.ts +15 -0
  657. package/src/modules/customers/subscribers/deal-closure-notification.ts +22 -0
  658. package/src/modules/customers/subscribers/deal-lost-notification.ts +22 -0
  659. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +2 -0
  660. package/src/modules/dictionaries/api/[dictionaryId]/entries/reorder/route.ts +162 -0
  661. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +6 -2
  662. package/src/modules/dictionaries/api/[dictionaryId]/entries/set-default/route.ts +162 -0
  663. package/src/modules/dictionaries/api/context.ts +9 -0
  664. package/src/modules/dictionaries/api/openapi.ts +17 -0
  665. package/src/modules/dictionaries/commands/entry-operations.ts +457 -0
  666. package/src/modules/dictionaries/commands/factory.ts +31 -3
  667. package/src/modules/dictionaries/commands/index.ts +1 -0
  668. package/src/modules/dictionaries/components/DictionaryTable.tsx +15 -6
  669. package/src/modules/dictionaries/data/entities.ts +9 -0
  670. package/src/modules/dictionaries/data/validators.ts +34 -0
  671. package/src/modules/dictionaries/events.ts +20 -0
  672. package/src/modules/dictionaries/i18n/de.json +2 -0
  673. package/src/modules/dictionaries/i18n/en.json +2 -0
  674. package/src/modules/dictionaries/i18n/es.json +2 -0
  675. package/src/modules/dictionaries/i18n/pl.json +2 -0
  676. package/src/modules/dictionaries/lib/clientEntries.ts +66 -0
  677. package/src/modules/dictionaries/migrations/.snapshot-open-mercato.json +185 -3
  678. package/src/modules/dictionaries/migrations/Migration20260410171544.ts +49 -0
  679. package/src/modules/inbox_ops/api/proposals/[id]/route.ts +4 -1
  680. package/src/modules/query_index/lib/engine.ts +9 -1
  681. package/src/modules/sales/components/documents/AddressesSection.tsx +92 -42
  682. package/src/modules/sales/i18n/de.json +28 -0
  683. package/src/modules/sales/i18n/en.json +28 -0
  684. package/src/modules/sales/i18n/es.json +28 -0
  685. package/src/modules/sales/i18n/pl.json +28 -0
  686. package/src/modules/sales/lib/dictionaries.ts +18 -0
  687. package/src/modules/sales/widgets/injection-table.ts +4 -0
@@ -0,0 +1,1372 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { Check, Plus, Search, SlidersHorizontal, Tag, X } from 'lucide-react'
5
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
6
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
7
+ import { cn, slugifyTagLabel } from '@open-mercato/shared/lib/utils'
8
+ import { apiCall, apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
9
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
11
+ import { Button } from '@open-mercato/ui/primitives/button'
12
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogTitle,
17
+ } from '@open-mercato/ui/primitives/dialog'
18
+ import type { TagSummary } from './types'
19
+ import { ManageTagsDialog } from './ManageTagsDialog'
20
+
21
+ type DictEntry = { id: string; value: string; label: string; color?: string | null }
22
+
23
+ type LabelItem = { id: string; slug: string; label: string }
24
+
25
+ type KindSetting = {
26
+ kind: string
27
+ selectionMode: 'single' | 'multi'
28
+ visibleInTags: boolean
29
+ sortOrder: number
30
+ }
31
+
32
+ type CategorySource = 'dictionary' | 'tags' | 'labels'
33
+
34
+ type EntityTagData = {
35
+ status?: string | null
36
+ lifecycleStage?: string | null
37
+ source?: string | null
38
+ temperature?: string | null
39
+ renewalQuarter?: string | null
40
+ jobTitle?: string | null
41
+ industry?: string | null
42
+ customFields?: Record<string, unknown>
43
+ tags?: TagSummary[]
44
+ }
45
+
46
+ type CategoryOption = {
47
+ id: string
48
+ value: string
49
+ label: string
50
+ color?: string | null
51
+ }
52
+
53
+ type CategoryDef = {
54
+ kind: string
55
+ source: CategorySource
56
+ supportedEntityTypes: Array<'person' | 'company'>
57
+ labelKey: string
58
+ labelFallback: string
59
+ descriptionKey: string
60
+ descriptionFallback: string
61
+ routeKind?: string
62
+ settingKind?: string
63
+ entityField?: keyof EntityTagData
64
+ customFieldKey?: string
65
+ selectionMode?: 'single' | 'multi'
66
+ hasColorDots: boolean
67
+ supportsCreate?: boolean
68
+ }
69
+
70
+ type CategorySection = CategoryDef & {
71
+ label: string
72
+ description: string
73
+ entries: CategoryOption[]
74
+ selectionMode: 'single' | 'multi'
75
+ }
76
+
77
+ export type EntityTagsDialogProps = {
78
+ open: boolean
79
+ onClose: () => void
80
+ entityId: string
81
+ entityType: 'person' | 'company'
82
+ entityOrganizationId: string | null
83
+ entityData: EntityTagData
84
+ onSaved?: () => void
85
+ }
86
+
87
+ const CATEGORY_DEFS: CategoryDef[] = [
88
+ {
89
+ kind: 'tags',
90
+ source: 'tags',
91
+ supportedEntityTypes: ['person', 'company'],
92
+ labelKey: 'customers.personTags.category.tags',
93
+ labelFallback: 'Tags',
94
+ descriptionKey: 'customers.personTags.description.tags',
95
+ descriptionFallback: 'Shared CRM tags that can be assigned to many records.',
96
+ selectionMode: 'multi',
97
+ hasColorDots: true,
98
+ supportsCreate: true,
99
+ },
100
+ {
101
+ kind: 'labels',
102
+ source: 'labels',
103
+ supportedEntityTypes: ['person', 'company'],
104
+ labelKey: 'customers.personTags.category.labels',
105
+ labelFallback: 'Labels',
106
+ descriptionKey: 'customers.personTags.description.labels',
107
+ descriptionFallback: 'Quick labels you can create inline for this record.',
108
+ selectionMode: 'multi',
109
+ hasColorDots: false,
110
+ supportsCreate: true,
111
+ },
112
+ {
113
+ kind: 'statuses',
114
+ source: 'dictionary',
115
+ supportedEntityTypes: ['person', 'company'],
116
+ labelKey: 'customers.personTags.category.statuses',
117
+ labelFallback: 'Status',
118
+ descriptionKey: 'customers.personTags.description.statuses',
119
+ descriptionFallback: 'Primary CRM status shown in the header badges.',
120
+ routeKind: 'statuses',
121
+ settingKind: 'status',
122
+ entityField: 'status',
123
+ selectionMode: 'single',
124
+ hasColorDots: true,
125
+ },
126
+ {
127
+ kind: 'lifecycle-stages',
128
+ source: 'dictionary',
129
+ supportedEntityTypes: ['person', 'company'],
130
+ labelKey: 'customers.personTags.category.lifecycle-stages',
131
+ labelFallback: 'Lifecycle',
132
+ descriptionKey: 'customers.personTags.description.lifecycleStages',
133
+ descriptionFallback: 'Lifecycle stage used across CRM detail views.',
134
+ routeKind: 'lifecycle-stages',
135
+ settingKind: 'lifecycle_stage',
136
+ entityField: 'lifecycleStage',
137
+ selectionMode: 'single',
138
+ hasColorDots: true,
139
+ },
140
+ {
141
+ kind: 'sources',
142
+ source: 'dictionary',
143
+ supportedEntityTypes: ['person', 'company'],
144
+ labelKey: 'customers.personTags.category.sources',
145
+ labelFallback: 'Source',
146
+ descriptionKey: 'customers.personTags.description.sources',
147
+ descriptionFallback: 'How this record entered the pipeline.',
148
+ routeKind: 'sources',
149
+ settingKind: 'source',
150
+ entityField: 'source',
151
+ selectionMode: 'single',
152
+ hasColorDots: true,
153
+ },
154
+ {
155
+ kind: 'temperature',
156
+ source: 'dictionary',
157
+ supportedEntityTypes: ['person', 'company'],
158
+ labelKey: 'customers.personTags.category.temperature',
159
+ labelFallback: 'Temperature',
160
+ descriptionKey: 'customers.personTags.description.temperature',
161
+ descriptionFallback: 'Sales temperature or engagement level.',
162
+ routeKind: 'temperature',
163
+ settingKind: 'temperature',
164
+ entityField: 'temperature',
165
+ selectionMode: 'single',
166
+ hasColorDots: true,
167
+ },
168
+ {
169
+ kind: 'renewal-quarters',
170
+ source: 'dictionary',
171
+ supportedEntityTypes: ['person', 'company'],
172
+ labelKey: 'customers.personTags.category.renewal-quarters',
173
+ labelFallback: 'Renewal quarter',
174
+ descriptionKey: 'customers.personTags.description.renewalQuarters',
175
+ descriptionFallback: 'Quarter used for renewal planning and badges.',
176
+ routeKind: 'renewal-quarters',
177
+ settingKind: 'renewal_quarter',
178
+ entityField: 'renewalQuarter',
179
+ selectionMode: 'single',
180
+ hasColorDots: true,
181
+ },
182
+ {
183
+ kind: 'job-titles',
184
+ source: 'dictionary',
185
+ supportedEntityTypes: ['person'],
186
+ labelKey: 'customers.personTags.category.job-titles',
187
+ labelFallback: 'Job title',
188
+ descriptionKey: 'customers.personTags.description.jobTitles',
189
+ descriptionFallback: 'The role or title used for this person.',
190
+ routeKind: 'job-titles',
191
+ settingKind: 'job_title',
192
+ entityField: 'jobTitle',
193
+ selectionMode: 'single',
194
+ hasColorDots: true,
195
+ },
196
+ {
197
+ kind: 'industries',
198
+ source: 'dictionary',
199
+ supportedEntityTypes: ['company'],
200
+ labelKey: 'customers.personTags.category.industries',
201
+ labelFallback: 'Industry',
202
+ descriptionKey: 'customers.personTags.description.industries',
203
+ descriptionFallback: 'The industry used to classify this company.',
204
+ routeKind: 'industries',
205
+ settingKind: 'industry',
206
+ entityField: 'industry',
207
+ selectionMode: 'single',
208
+ hasColorDots: true,
209
+ },
210
+ ]
211
+
212
+ const REMOTE_CATEGORY_PAGE_SIZE = 50
213
+ const CUSTOM_CATEGORY_FIELD_PREFIX = 'crmTagCategory:'
214
+
215
+ function cloneSelectionMap(values: Record<string, Set<string>>): Record<string, Set<string>> {
216
+ return Object.fromEntries(
217
+ Object.entries(values).map(([key, selection]) => [key, new Set(selection)]),
218
+ )
219
+ }
220
+
221
+ function sortOptions(entries: CategoryOption[]): CategoryOption[] {
222
+ return [...entries].sort((left, right) =>
223
+ left.label.localeCompare(right.label, undefined, { sensitivity: 'base' }),
224
+ )
225
+ }
226
+
227
+ function mergeOptions(...groups: CategoryOption[][]): CategoryOption[] {
228
+ const merged = new Map<string, CategoryOption>()
229
+ groups.flat().forEach((entry) => {
230
+ merged.set(entry.value, entry)
231
+ })
232
+ return sortOptions(Array.from(merged.values()))
233
+ }
234
+
235
+ function humanizeCategoryKind(kind: string): string {
236
+ return kind
237
+ .split(/[-_]+/)
238
+ .filter((part) => part.trim().length > 0)
239
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
240
+ .join(' ')
241
+ }
242
+
243
+ function resolveCustomCategoryFieldKey(kind: string): string {
244
+ return `${CUSTOM_CATEGORY_FIELD_PREFIX}${kind}`
245
+ }
246
+
247
+ function createCustomCategoryDef(kind: string, selectionMode: 'single' | 'multi'): CategoryDef {
248
+ const label = humanizeCategoryKind(kind)
249
+ return {
250
+ kind,
251
+ source: 'dictionary',
252
+ supportedEntityTypes: ['person', 'company'],
253
+ labelKey: '',
254
+ labelFallback: label,
255
+ descriptionKey: 'customers.personTags.description.customCategory',
256
+ descriptionFallback: `Custom CRM category: ${label}.`,
257
+ routeKind: kind,
258
+ settingKind: kind,
259
+ customFieldKey: resolveCustomCategoryFieldKey(kind),
260
+ selectionMode,
261
+ hasColorDots: true,
262
+ }
263
+ }
264
+
265
+ function normalizeSelectionValues(
266
+ value: unknown,
267
+ selectionMode: 'single' | 'multi',
268
+ ): string[] {
269
+ if (typeof value === 'string') {
270
+ const trimmed = value.trim()
271
+ return trimmed.length > 0 ? [trimmed] : []
272
+ }
273
+ if (Array.isArray(value)) {
274
+ const normalized = value
275
+ .filter((entry): entry is string => typeof entry === 'string')
276
+ .map((entry) => entry.trim())
277
+ .filter((entry) => entry.length > 0)
278
+ if (selectionMode === 'single') {
279
+ return normalized.length > 0 ? [normalized[0]] : []
280
+ }
281
+ return normalized
282
+ }
283
+ return []
284
+ }
285
+
286
+ function readCategorySelectionValues(
287
+ category: CategoryDef,
288
+ entityData: EntityTagData,
289
+ selectionMode: 'single' | 'multi',
290
+ ): string[] {
291
+ if (category.entityField) {
292
+ return normalizeSelectionValues(entityData[category.entityField], selectionMode)
293
+ }
294
+ if (category.customFieldKey) {
295
+ return normalizeSelectionValues(entityData.customFields?.[category.customFieldKey], selectionMode)
296
+ }
297
+ return []
298
+ }
299
+
300
+ function areSelectionsEqual(left: Set<string>, right: Set<string>): boolean {
301
+ if (left.size !== right.size) return false
302
+ for (const value of left) {
303
+ if (!right.has(value)) return false
304
+ }
305
+ return true
306
+ }
307
+
308
+ function TagChip({
309
+ label,
310
+ color,
311
+ active,
312
+ showColorDot,
313
+ onClick,
314
+ }: {
315
+ label: string
316
+ color?: string | null
317
+ active: boolean
318
+ showColorDot: boolean
319
+ onClick: () => void
320
+ }) {
321
+ const activeColorStyle: React.CSSProperties | undefined =
322
+ active && color
323
+ ? { color, borderColor: color, backgroundColor: `${color}1A` }
324
+ : undefined
325
+ return (
326
+ <Button
327
+ type="button"
328
+ variant="ghost"
329
+ size="sm"
330
+ onClick={onClick}
331
+ className={cn(
332
+ 'inline-flex h-auto items-center gap-1 rounded-full border px-2.5 py-1.5 transition-colors',
333
+ active
334
+ ? activeColorStyle
335
+ ? 'font-semibold hover:opacity-90'
336
+ : 'border-transparent bg-muted font-semibold text-foreground hover:bg-muted'
337
+ : 'border-border bg-transparent font-normal text-muted-foreground hover:bg-muted/60 hover:text-foreground',
338
+ )}
339
+ style={activeColorStyle}
340
+ >
341
+ {showColorDot && color ? (
342
+ <span
343
+ className="inline-block size-2 shrink-0 rounded-full"
344
+ style={{ backgroundColor: color }}
345
+ />
346
+ ) : null}
347
+ <span className="text-xs">{label}</span>
348
+ {active ? <X className="size-2.5 shrink-0" /> : null}
349
+ </Button>
350
+ )
351
+ }
352
+
353
+ function buildApplicableCategories(
354
+ entityType: 'person' | 'company',
355
+ kindSettings: KindSetting[],
356
+ ) {
357
+ const baseCategories = CATEGORY_DEFS.filter((category) => category.supportedEntityTypes.includes(entityType))
358
+ const customCategories = kindSettings
359
+ .filter((setting) => setting.visibleInTags)
360
+ .filter((setting) => !CATEGORY_DEFS.some((category) => category.kind === setting.kind || category.settingKind === setting.kind))
361
+ .map((setting) => createCustomCategoryDef(setting.kind, setting.selectionMode))
362
+ return [...baseCategories, ...customCategories]
363
+ }
364
+
365
+ export function EntityTagsDialog({
366
+ open,
367
+ onClose,
368
+ entityId,
369
+ entityType,
370
+ entityOrganizationId,
371
+ entityData,
372
+ onSaved,
373
+ }: EntityTagsDialogProps) {
374
+ const t = useT()
375
+ const [loading, setLoading] = React.useState(true)
376
+ const [saving, setSaving] = React.useState(false)
377
+ const [searchValue, setSearchValue] = React.useState('')
378
+ const [categories, setCategories] = React.useState<CategorySection[]>([])
379
+ const [selectedEntrySeeds, setSelectedEntrySeeds] = React.useState<Record<string, CategoryOption[]>>({})
380
+ const [selectedValues, setSelectedValues] = React.useState<Record<string, Set<string>>>({})
381
+ const [originalValues, setOriginalValues] = React.useState<Record<string, Set<string>>>({})
382
+ const [activeCategoryKind, setActiveCategoryKind] = React.useState<string | null>(null)
383
+ const [newEntryInputByKind, setNewEntryInputByKind] = React.useState<Record<string, string | null>>({})
384
+ const [creatingKind, setCreatingKind] = React.useState<string | null>(null)
385
+ const [manageTagsOpen, setManageTagsOpen] = React.useState(false)
386
+ const [activeCategoryPage, setActiveCategoryPage] = React.useState(1)
387
+ const [activeCategoryTotalPages, setActiveCategoryTotalPages] = React.useState(1)
388
+ const [activeCategoryLoading, setActiveCategoryLoading] = React.useState(false)
389
+ const creationInFlightRef = React.useRef<string | null>(null)
390
+ const mutationContextId = React.useMemo(
391
+ () => `customer-tags:${entityType}:${entityId}`,
392
+ [entityId, entityType],
393
+ )
394
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
395
+ formId: string
396
+ resourceKind: string
397
+ resourceId: string
398
+ entityType: 'person' | 'company'
399
+ retryLastMutation: () => Promise<boolean>
400
+ }>({
401
+ contextId: mutationContextId,
402
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
403
+ })
404
+ const mutationContext = React.useMemo(
405
+ () => ({
406
+ formId: mutationContextId,
407
+ resourceKind: entityType === 'person' ? 'customers.person' : 'customers.company',
408
+ resourceId: entityId,
409
+ entityType,
410
+ retryLastMutation,
411
+ }),
412
+ [entityId, entityType, mutationContextId, retryLastMutation],
413
+ )
414
+ const runGuardedMutation = React.useCallback(
415
+ async <T,>(operation: () => Promise<T>, mutationPayload: Record<string, unknown>) =>
416
+ runMutation({
417
+ operation,
418
+ mutationPayload,
419
+ context: mutationContext,
420
+ }),
421
+ [mutationContext, runMutation],
422
+ )
423
+
424
+ const updateCategoryEntries = React.useCallback(
425
+ (kind: string, updater: (entries: CategoryOption[]) => CategoryOption[]) => {
426
+ setCategories((previous) =>
427
+ previous.map((category) =>
428
+ category.kind === kind
429
+ ? { ...category, entries: sortOptions(updater(category.entries)) }
430
+ : category,
431
+ ),
432
+ )
433
+ },
434
+ [],
435
+ )
436
+
437
+ const loadData = React.useCallback(async () => {
438
+ setLoading(true)
439
+ try {
440
+ let kindSettings: KindSetting[] = []
441
+ const scopedQuery = new URLSearchParams()
442
+ if (entityOrganizationId) {
443
+ scopedQuery.set('organizationId', entityOrganizationId)
444
+ }
445
+ try {
446
+ const settingsCall = await apiCall<{ items?: KindSetting[] }>(
447
+ `/api/customers/dictionaries/kind-settings${scopedQuery.size ? `?${scopedQuery.toString()}` : ''}`,
448
+ { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
449
+ )
450
+ if (settingsCall.ok && settingsCall.result?.items) {
451
+ kindSettings = settingsCall.result.items
452
+ }
453
+ } catch {
454
+ // Default category order works without explicit settings rows.
455
+ }
456
+
457
+ const settingsMap = new Map(kindSettings.map((setting) => [setting.kind, setting]))
458
+
459
+ const selectedTagEntries = Array.isArray(entityData.tags)
460
+ ? entityData.tags.map((tag) => ({
461
+ id: tag.id,
462
+ value: tag.id,
463
+ label: tag.label,
464
+ color: tag.color ?? null,
465
+ }))
466
+ : []
467
+
468
+ let assignedLabelIds: string[] = []
469
+ let selectedLabelEntries: CategoryOption[] = []
470
+ try {
471
+ const labelsQuery = new URLSearchParams()
472
+ labelsQuery.set('entityId', entityId)
473
+ labelsQuery.set('pageSize', '1')
474
+ if (entityOrganizationId) {
475
+ labelsQuery.set('organizationId', entityOrganizationId)
476
+ }
477
+ const labelsCall = await apiCall<{
478
+ items?: LabelItem[]
479
+ assignedIds?: string[]
480
+ }>(`/api/customers/labels?${labelsQuery.toString()}`, {
481
+ cache: 'no-store',
482
+ headers: { 'x-om-unauthorized-redirect': '0' },
483
+ })
484
+ const labelsData = labelsCall.ok ? labelsCall.result : null
485
+ assignedLabelIds = labelsData?.assignedIds ?? []
486
+ if (assignedLabelIds.length > 0) {
487
+ const detailQuery = new URLSearchParams({
488
+ ids: assignedLabelIds.join(','),
489
+ pageSize: String(Math.min(assignedLabelIds.length, 100)),
490
+ })
491
+ if (entityOrganizationId) {
492
+ detailQuery.set('organizationId', entityOrganizationId)
493
+ }
494
+ const selectedLabelsCall = await apiCall<{ items?: LabelItem[] }>(
495
+ `/api/customers/labels?${detailQuery.toString()}`,
496
+ {
497
+ cache: 'no-store',
498
+ headers: { 'x-om-unauthorized-redirect': '0' },
499
+ },
500
+ )
501
+ const selectedLabels = selectedLabelsCall.ok ? selectedLabelsCall.result?.items ?? [] : []
502
+ selectedLabelEntries = selectedLabels.map((label) => ({
503
+ id: label.id,
504
+ value: label.id,
505
+ label: label.label,
506
+ color: null,
507
+ }))
508
+ }
509
+ } catch {
510
+ assignedLabelIds = []
511
+ selectedLabelEntries = []
512
+ }
513
+
514
+ const categoryDefs = buildApplicableCategories(entityType, kindSettings)
515
+ const loadedCategories: CategorySection[] = []
516
+
517
+ for (const categoryDef of categoryDefs) {
518
+ if (categoryDef.source === 'tags') {
519
+ loadedCategories.push({
520
+ ...categoryDef,
521
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
522
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
523
+ entries: sortOptions(selectedTagEntries),
524
+ selectionMode: categoryDef.selectionMode ?? 'multi',
525
+ })
526
+ continue
527
+ }
528
+
529
+ if (categoryDef.source === 'labels') {
530
+ loadedCategories.push({
531
+ ...categoryDef,
532
+ label: t(categoryDef.labelKey, categoryDef.labelFallback),
533
+ description: t(categoryDef.descriptionKey, categoryDef.descriptionFallback),
534
+ entries: sortOptions(selectedLabelEntries),
535
+ selectionMode: categoryDef.selectionMode ?? 'multi',
536
+ })
537
+ continue
538
+ }
539
+
540
+ try {
541
+ const dictionaryUrl = new URL(`/api/customers/dictionaries/${categoryDef.routeKind}`, 'http://localhost')
542
+ if (entityOrganizationId) {
543
+ dictionaryUrl.searchParams.set('organizationId', entityOrganizationId)
544
+ }
545
+ const dictionaryCall = await apiCall<{ items?: DictEntry[] }>(
546
+ `${dictionaryUrl.pathname}${dictionaryUrl.search}`,
547
+ { cache: 'no-store', headers: { 'x-om-unauthorized-redirect': '0' } },
548
+ )
549
+ const dictionaryItems = dictionaryCall.ok ? dictionaryCall.result?.items ?? [] : []
550
+ const entries = dictionaryItems.map((entry) => ({
551
+ id: entry.id,
552
+ value: entry.value,
553
+ label: entry.label,
554
+ color: entry.color ?? null,
555
+ }))
556
+ const setting = categoryDef.settingKind
557
+ ? settingsMap.get(categoryDef.settingKind)
558
+ : undefined
559
+ const selectionMode = setting?.selectionMode ?? categoryDef.selectionMode ?? 'single'
560
+ const currentValues = readCategorySelectionValues(categoryDef, entityData, selectionMode)
561
+ currentValues.forEach((currentValue) => {
562
+ if (entries.some((entry) => entry.value === currentValue)) return
563
+ entries.push({
564
+ id: `current:${categoryDef.kind}:${currentValue}`,
565
+ value: currentValue,
566
+ label: currentValue,
567
+ color: null,
568
+ })
569
+ })
570
+ loadedCategories.push({
571
+ ...categoryDef,
572
+ label: categoryDef.labelKey
573
+ ? t(categoryDef.labelKey, categoryDef.labelFallback)
574
+ : categoryDef.labelFallback,
575
+ description: categoryDef.descriptionKey
576
+ ? t(
577
+ categoryDef.descriptionKey,
578
+ categoryDef.descriptionFallback,
579
+ categoryDef.customFieldKey ? { name: categoryDef.labelFallback } : undefined,
580
+ )
581
+ : categoryDef.descriptionFallback,
582
+ entries: sortOptions(entries),
583
+ selectionMode,
584
+ })
585
+ } catch {
586
+ const setting = categoryDef.settingKind
587
+ ? settingsMap.get(categoryDef.settingKind)
588
+ : undefined
589
+ const selectionMode = setting?.selectionMode ?? categoryDef.selectionMode ?? 'single'
590
+ const fallbackEntries = readCategorySelectionValues(categoryDef, entityData, selectionMode).map((value) => ({
591
+ id: `current:${categoryDef.kind}:${value}`,
592
+ value,
593
+ label: value,
594
+ color: null,
595
+ }))
596
+ loadedCategories.push({
597
+ ...categoryDef,
598
+ label: categoryDef.labelKey
599
+ ? t(categoryDef.labelKey, categoryDef.labelFallback)
600
+ : categoryDef.labelFallback,
601
+ description: categoryDef.descriptionKey
602
+ ? t(
603
+ categoryDef.descriptionKey,
604
+ categoryDef.descriptionFallback,
605
+ categoryDef.customFieldKey ? { name: categoryDef.labelFallback } : undefined,
606
+ )
607
+ : categoryDef.descriptionFallback,
608
+ entries: fallbackEntries,
609
+ selectionMode,
610
+ })
611
+ }
612
+ }
613
+
614
+ loadedCategories.sort((left, right) => {
615
+ const leftSortOrder =
616
+ left.settingKind && settingsMap.has(left.settingKind)
617
+ ? settingsMap.get(left.settingKind)?.sortOrder ?? 1000
618
+ : 1000 + CATEGORY_DEFS.findIndex((category) => category.kind === left.kind)
619
+ const rightSortOrder =
620
+ right.settingKind && settingsMap.has(right.settingKind)
621
+ ? settingsMap.get(right.settingKind)?.sortOrder ?? 1000
622
+ : 1000 + CATEGORY_DEFS.findIndex((category) => category.kind === right.kind)
623
+ return leftSortOrder - rightSortOrder
624
+ })
625
+
626
+ const initialValues: Record<string, Set<string>> = {}
627
+ for (const category of loadedCategories) {
628
+ if (category.source === 'dictionary' && (category.entityField || category.customFieldKey)) {
629
+ initialValues[category.kind] = new Set(
630
+ readCategorySelectionValues(category, entityData, category.selectionMode),
631
+ )
632
+ continue
633
+ }
634
+ if (category.source === 'tags') {
635
+ const availableTagIds = new Set(category.entries.map((entry) => entry.value))
636
+ const assignedTagIds = Array.isArray(entityData.tags)
637
+ ? entityData.tags
638
+ .map((tag) => tag.id)
639
+ .filter((tagId) => availableTagIds.has(tagId))
640
+ : []
641
+ initialValues[category.kind] = new Set(assignedTagIds)
642
+ continue
643
+ }
644
+ if (category.source === 'labels') {
645
+ const availableLabelIds = new Set(category.entries.map((entry) => entry.value))
646
+ initialValues[category.kind] = new Set(
647
+ assignedLabelIds.filter((labelId) => availableLabelIds.has(labelId)),
648
+ )
649
+ continue
650
+ }
651
+ initialValues[category.kind] = new Set()
652
+ }
653
+
654
+ setCategories(loadedCategories)
655
+ setSelectedEntrySeeds({
656
+ tags: selectedTagEntries,
657
+ labels: selectedLabelEntries,
658
+ })
659
+ setSelectedValues(initialValues)
660
+ setOriginalValues(cloneSelectionMap(initialValues))
661
+ setActiveCategoryKind((previous) => {
662
+ if (previous && loadedCategories.some((category) => category.kind === previous)) {
663
+ return previous
664
+ }
665
+ return loadedCategories[0]?.kind ?? null
666
+ })
667
+ } finally {
668
+ setLoading(false)
669
+ }
670
+ }, [entityData, entityId, entityOrganizationId, entityType, t])
671
+
672
+ React.useEffect(() => {
673
+ if (!open) return
674
+ setSearchValue('')
675
+ setNewEntryInputByKind({})
676
+ loadData().catch((err) => console.warn('[EntityTagsDialog] loadData failed', err))
677
+ }, [loadData, open])
678
+
679
+ React.useEffect(() => {
680
+ if (open) return
681
+ setManageTagsOpen(false)
682
+ }, [open])
683
+
684
+ const activeCategory = React.useMemo(() => {
685
+ if (!categories.length) return null
686
+ return categories.find((category) => category.kind === activeCategoryKind) ?? categories[0]
687
+ }, [activeCategoryKind, categories])
688
+ const activeCategoryKindValue = activeCategory?.kind ?? null
689
+ const activeCategorySource = activeCategory?.source ?? null
690
+
691
+ React.useEffect(() => {
692
+ if (!open) return
693
+ setActiveCategoryPage(1)
694
+ }, [activeCategoryKind, open, searchValue])
695
+
696
+ React.useEffect(() => {
697
+ if (!open || !activeCategoryKindValue || (activeCategorySource !== 'tags' && activeCategorySource !== 'labels')) {
698
+ setActiveCategoryLoading(false)
699
+ setActiveCategoryTotalPages(1)
700
+ return
701
+ }
702
+
703
+ let cancelled = false
704
+ const params = new URLSearchParams({
705
+ page: String(activeCategoryPage),
706
+ pageSize: String(REMOTE_CATEGORY_PAGE_SIZE),
707
+ })
708
+ if (searchValue.trim().length > 0) {
709
+ params.set('search', searchValue.trim())
710
+ }
711
+ if (activeCategorySource === 'labels') {
712
+ params.set('entityId', entityId)
713
+ if (entityOrganizationId) {
714
+ params.set('organizationId', entityOrganizationId)
715
+ }
716
+ }
717
+
718
+ const endpoint =
719
+ activeCategorySource === 'tags'
720
+ ? `/api/customers/tags?${params.toString()}`
721
+ : `/api/customers/labels?${params.toString()}`
722
+ const seedEntries = searchValue.trim().length > 0 ? [] : selectedEntrySeeds[activeCategoryKindValue] ?? []
723
+
724
+ const mapEntries = (items: Array<DictEntry | LabelItem>): CategoryOption[] =>
725
+ items.map((entry) => ({
726
+ id: entry.id,
727
+ value: entry.id,
728
+ label: entry.label,
729
+ color: activeCategorySource === 'tags' && 'color' in entry ? entry.color ?? null : null,
730
+ }))
731
+
732
+ setActiveCategoryLoading(true)
733
+ void apiCall<{ items?: Array<DictEntry | LabelItem>; totalPages?: number }>(endpoint, {
734
+ cache: 'no-store',
735
+ headers: { 'x-om-unauthorized-redirect': '0' },
736
+ })
737
+ .then((response) => {
738
+ if (!response.ok || cancelled) return
739
+ const fetchedEntries = mapEntries(Array.isArray(response.result?.items) ? response.result.items : [])
740
+ setActiveCategoryTotalPages(
741
+ typeof response.result?.totalPages === 'number' ? response.result.totalPages : 1,
742
+ )
743
+ updateCategoryEntries(activeCategoryKindValue, (currentEntries) =>
744
+ activeCategoryPage <= 1
745
+ ? mergeOptions(seedEntries, fetchedEntries)
746
+ : mergeOptions(currentEntries, seedEntries, fetchedEntries),
747
+ )
748
+ })
749
+ .catch(() => {
750
+ if (cancelled) return
751
+ setActiveCategoryTotalPages(1)
752
+ updateCategoryEntries(activeCategoryKindValue, () => seedEntries)
753
+ })
754
+ .finally(() => {
755
+ if (!cancelled) {
756
+ setActiveCategoryLoading(false)
757
+ }
758
+ })
759
+
760
+ return () => {
761
+ cancelled = true
762
+ }
763
+ }, [
764
+ activeCategoryKindValue,
765
+ activeCategoryPage,
766
+ activeCategorySource,
767
+ entityId,
768
+ entityOrganizationId,
769
+ open,
770
+ searchValue,
771
+ selectedEntrySeeds,
772
+ updateCategoryEntries,
773
+ ])
774
+
775
+ const activeCount = React.useMemo(
776
+ () => Object.values(selectedValues).reduce((count, values) => count + values.size, 0),
777
+ [selectedValues],
778
+ )
779
+
780
+ const hasChanges = React.useMemo(() => {
781
+ const kinds = new Set([
782
+ ...Object.keys(selectedValues),
783
+ ...Object.keys(originalValues),
784
+ ])
785
+ for (const kind of kinds) {
786
+ const current = selectedValues[kind] ?? new Set<string>()
787
+ const original = originalValues[kind] ?? new Set<string>()
788
+ if (current.size !== original.size) return true
789
+ for (const value of current) {
790
+ if (!original.has(value)) return true
791
+ }
792
+ }
793
+ return false
794
+ }, [originalValues, selectedValues])
795
+
796
+ const filteredEntries = React.useMemo(() => {
797
+ if (!activeCategory) return []
798
+ const query = searchValue.trim().toLowerCase()
799
+ if (!query) return activeCategory.entries
800
+ return activeCategory.entries.filter((entry) =>
801
+ entry.label.toLowerCase().includes(query) || entry.value.toLowerCase().includes(query),
802
+ )
803
+ }, [activeCategory, searchValue])
804
+
805
+ const toggleValue = React.useCallback(
806
+ (kind: string, value: string, selectionMode: 'single' | 'multi') => {
807
+ setSelectedValues((previous) => {
808
+ const next = new Set(previous[kind] ?? [])
809
+ if (next.has(value)) {
810
+ next.delete(value)
811
+ } else {
812
+ if (selectionMode === 'single') {
813
+ next.clear()
814
+ }
815
+ next.add(value)
816
+ }
817
+ return { ...previous, [kind]: next }
818
+ })
819
+ },
820
+ [],
821
+ )
822
+
823
+ const handleCreateEntry = React.useCallback(async () => {
824
+ if (!activeCategory || !activeCategory.supportsCreate) return
825
+ const draftValue = newEntryInputByKind[activeCategory.kind] ?? ''
826
+ const trimmed = draftValue.trim()
827
+ if (!trimmed || creatingKind || creationInFlightRef.current) {
828
+ if (!trimmed) {
829
+ setNewEntryInputByKind((previous) => ({ ...previous, [activeCategory.kind]: null }))
830
+ }
831
+ return
832
+ }
833
+
834
+ const existingEntry = activeCategory.entries.find(
835
+ (entry) => entry.label.trim().toLowerCase() === trimmed.toLowerCase(),
836
+ )
837
+ if (existingEntry) {
838
+ setSelectedValues((previous) => {
839
+ const current = new Set(previous[activeCategory.kind] ?? [])
840
+ current.add(existingEntry.value)
841
+ return { ...previous, [activeCategory.kind]: current }
842
+ })
843
+ setNewEntryInputByKind((previous) => ({ ...previous, [activeCategory.kind]: null }))
844
+ return
845
+ }
846
+
847
+ creationInFlightRef.current = activeCategory.kind
848
+ setCreatingKind(activeCategory.kind)
849
+ try {
850
+ if (activeCategory.source === 'tags') {
851
+ const result = await runGuardedMutation(
852
+ () =>
853
+ readApiResultOrThrow<Record<string, unknown>>(
854
+ '/api/customers/tags',
855
+ {
856
+ method: 'POST',
857
+ headers: { 'content-type': 'application/json' },
858
+ body: JSON.stringify({
859
+ label: trimmed,
860
+ slug: slugifyTagLabel(trimmed),
861
+ }),
862
+ },
863
+ ),
864
+ { categoryKind: activeCategory.kind, operation: 'createTag', label: trimmed },
865
+ )
866
+ const id = typeof result?.id === 'string' ? result.id : ''
867
+ if (!id) {
868
+ throw new Error(t('customers.people.detail.tags.createError', 'Failed to create tag.'))
869
+ }
870
+ const option: CategoryOption = {
871
+ id,
872
+ value: id,
873
+ label: trimmed,
874
+ color: typeof result?.color === 'string' ? result.color : null,
875
+ }
876
+ updateCategoryEntries(activeCategory.kind, (entries) => [...entries, option])
877
+ setSelectedEntrySeeds((previous) => ({
878
+ ...previous,
879
+ [activeCategory.kind]: mergeOptions(previous[activeCategory.kind] ?? [], [option]),
880
+ }))
881
+ setSelectedValues((previous) => ({
882
+ ...previous,
883
+ [activeCategory.kind]: new Set([...(previous[activeCategory.kind] ?? []), option.value]),
884
+ }))
885
+ } else if (activeCategory.source === 'labels') {
886
+ const payload = entityOrganizationId
887
+ ? { label: trimmed, organizationId: entityOrganizationId }
888
+ : { label: trimmed }
889
+ const result = await runGuardedMutation(
890
+ () =>
891
+ readApiResultOrThrow<{ id: string; slug: string; label: string }>(
892
+ '/api/customers/labels',
893
+ {
894
+ method: 'POST',
895
+ headers: { 'content-type': 'application/json' },
896
+ body: JSON.stringify(payload),
897
+ },
898
+ ),
899
+ { categoryKind: activeCategory.kind, operation: 'createLabel', label: trimmed },
900
+ )
901
+ const option: CategoryOption = {
902
+ id: result.id,
903
+ value: result.id,
904
+ label: result.label,
905
+ color: null,
906
+ }
907
+ updateCategoryEntries(activeCategory.kind, (entries) => [...entries, option])
908
+ setSelectedEntrySeeds((previous) => ({
909
+ ...previous,
910
+ [activeCategory.kind]: mergeOptions(previous[activeCategory.kind] ?? [], [option]),
911
+ }))
912
+ setSelectedValues((previous) => ({
913
+ ...previous,
914
+ [activeCategory.kind]: new Set([...(previous[activeCategory.kind] ?? []), option.value]),
915
+ }))
916
+ }
917
+ setNewEntryInputByKind((previous) => ({ ...previous, [activeCategory.kind]: null }))
918
+ } catch (error) {
919
+ const message =
920
+ error instanceof Error
921
+ ? error.message
922
+ : t('customers.personTags.createLabelError', 'Failed to create label')
923
+ flash(message, 'error')
924
+ } finally {
925
+ creationInFlightRef.current = null
926
+ setCreatingKind(null)
927
+ }
928
+ }, [activeCategory, creatingKind, entityOrganizationId, newEntryInputByKind, runGuardedMutation, t, updateCategoryEntries])
929
+
930
+ const handleSave = React.useCallback(async () => {
931
+ if (saving) return
932
+ setSaving(true)
933
+ try {
934
+ const entityUpdate: Record<string, string | null> = {}
935
+ const customFieldUpdate: Record<string, unknown> = {}
936
+ categories.forEach((category) => {
937
+ if (category.source !== 'dictionary') return
938
+ const currentSelection = selectedValues[category.kind] ?? new Set<string>()
939
+ const originalSelection = originalValues[category.kind] ?? new Set<string>()
940
+ if (areSelectionsEqual(currentSelection, originalSelection)) return
941
+ if (category.entityField) {
942
+ const currentValue = currentSelection.size > 0 ? Array.from(currentSelection)[0] ?? null : null
943
+ entityUpdate[category.entityField] = currentValue
944
+ return
945
+ }
946
+ if (category.customFieldKey) {
947
+ customFieldUpdate[category.customFieldKey] =
948
+ category.selectionMode === 'single'
949
+ ? Array.from(currentSelection)[0] ?? null
950
+ : Array.from(currentSelection)
951
+ }
952
+ })
953
+
954
+ if (Object.keys(entityUpdate).length > 0 || Object.keys(customFieldUpdate).length > 0) {
955
+ await runGuardedMutation(
956
+ () =>
957
+ apiCallOrThrow(`/api/customers/${entityType === 'person' ? 'people' : 'companies'}`, {
958
+ method: 'PUT',
959
+ headers: { 'content-type': 'application/json' },
960
+ body: JSON.stringify({
961
+ id: entityId,
962
+ ...entityUpdate,
963
+ ...(Object.keys(customFieldUpdate).length > 0
964
+ ? {
965
+ customFields: {
966
+ ...(entityData.customFields ?? {}),
967
+ ...customFieldUpdate,
968
+ },
969
+ }
970
+ : {}),
971
+ }),
972
+ }),
973
+ { operation: 'updateEntityTags', entityUpdate, customFieldUpdate },
974
+ )
975
+ }
976
+
977
+ const currentTags = selectedValues.tags ?? new Set<string>()
978
+ const originalTags = originalValues.tags ?? new Set<string>()
979
+ const addedTags = Array.from(currentTags).filter((tagId) => !originalTags.has(tagId))
980
+ const removedTags = Array.from(originalTags).filter((tagId) => !currentTags.has(tagId))
981
+
982
+ for (const tagId of addedTags) {
983
+ await runGuardedMutation(
984
+ () =>
985
+ apiCallOrThrow('/api/customers/tags/assign', {
986
+ method: 'POST',
987
+ headers: { 'content-type': 'application/json' },
988
+ body: JSON.stringify({ tagId, entityId }),
989
+ }),
990
+ { operation: 'assignTag', tagId },
991
+ )
992
+ }
993
+
994
+ for (const tagId of removedTags) {
995
+ await runGuardedMutation(
996
+ () =>
997
+ apiCallOrThrow('/api/customers/tags/unassign', {
998
+ method: 'POST',
999
+ headers: { 'content-type': 'application/json' },
1000
+ body: JSON.stringify({ tagId, entityId }),
1001
+ }),
1002
+ { operation: 'unassignTag', tagId },
1003
+ )
1004
+ }
1005
+
1006
+ const currentLabels = selectedValues.labels ?? new Set<string>()
1007
+ const originalLabels = originalValues.labels ?? new Set<string>()
1008
+ const addedLabels = Array.from(currentLabels).filter((labelId) => !originalLabels.has(labelId))
1009
+ const removedLabels = Array.from(originalLabels).filter((labelId) => !currentLabels.has(labelId))
1010
+
1011
+ for (const labelId of addedLabels) {
1012
+ const payload = entityOrganizationId
1013
+ ? { labelId, entityId, organizationId: entityOrganizationId }
1014
+ : { labelId, entityId }
1015
+ await runGuardedMutation(
1016
+ () =>
1017
+ apiCallOrThrow('/api/customers/labels/assign', {
1018
+ method: 'POST',
1019
+ headers: { 'content-type': 'application/json' },
1020
+ body: JSON.stringify(payload),
1021
+ }),
1022
+ { operation: 'assignLabel', labelId },
1023
+ )
1024
+ }
1025
+
1026
+ for (const labelId of removedLabels) {
1027
+ const payload = entityOrganizationId
1028
+ ? { labelId, entityId, organizationId: entityOrganizationId }
1029
+ : { labelId, entityId }
1030
+ await runGuardedMutation(
1031
+ () =>
1032
+ apiCallOrThrow('/api/customers/labels/unassign', {
1033
+ method: 'POST',
1034
+ headers: { 'content-type': 'application/json' },
1035
+ body: JSON.stringify(payload),
1036
+ }),
1037
+ { operation: 'unassignLabel', labelId },
1038
+ )
1039
+ }
1040
+
1041
+ flash(t('customers.personTags.saveSuccess', 'Tags updated.'), 'success')
1042
+ onSaved?.()
1043
+ onClose()
1044
+ } catch (error) {
1045
+ const message =
1046
+ error instanceof Error
1047
+ ? error.message
1048
+ : t('customers.personTags.saveError', 'Failed to save tags')
1049
+ flash(message, 'error')
1050
+ } finally {
1051
+ setSaving(false)
1052
+ }
1053
+ }, [
1054
+ categories,
1055
+ entityId,
1056
+ entityOrganizationId,
1057
+ entityType,
1058
+ onClose,
1059
+ onSaved,
1060
+ originalValues,
1061
+ saving,
1062
+ selectedValues,
1063
+ runGuardedMutation,
1064
+ t,
1065
+ ])
1066
+
1067
+ React.useEffect(() => {
1068
+ if (!open) return
1069
+ const handler = (event: KeyboardEvent) => {
1070
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
1071
+ event.preventDefault()
1072
+ void handleSave()
1073
+ }
1074
+ }
1075
+ window.addEventListener('keydown', handler)
1076
+ return () => window.removeEventListener('keydown', handler)
1077
+ }, [handleSave, open])
1078
+
1079
+ React.useEffect(() => {
1080
+ if (!open) return
1081
+ setSearchValue('')
1082
+ }, [activeCategoryKind, open])
1083
+
1084
+ const activeSelection = activeCategory
1085
+ ? selectedValues[activeCategory.kind] ?? new Set<string>()
1086
+ : new Set<string>()
1087
+
1088
+ return (
1089
+ <Dialog open={open} onOpenChange={(next) => { if (!next) onClose() }}>
1090
+ <DialogContent
1091
+ className="flex max-h-[85vh] flex-col overflow-hidden border-border bg-background p-0 shadow-[0px_16px_40px_0px_rgba(0,0,0,0.14)] sm:max-w-[760px] sm:rounded-xl [&>[data-dialog-close]]:hidden"
1092
+ aria-describedby={undefined}
1093
+ >
1094
+ <VisuallyHidden>
1095
+ <DialogTitle>{t('customers.personTags.title', 'Edit tags')}</DialogTitle>
1096
+ </VisuallyHidden>
1097
+
1098
+ <div className="flex shrink-0 items-center justify-between border-b border-border bg-card px-5 py-4">
1099
+ <div className="flex items-center gap-2">
1100
+ <Tag className="size-4 text-foreground" />
1101
+ <span className="text-sm font-bold text-foreground">
1102
+ {t('customers.personTags.title', 'Edit tags')}
1103
+ </span>
1104
+ </div>
1105
+ <div className="flex items-center gap-2">
1106
+ <Button
1107
+ type="button"
1108
+ variant="outline"
1109
+ size="sm"
1110
+ className="h-auto gap-2 rounded-lg px-3.5 py-2 text-sm font-medium"
1111
+ onClick={() => setManageTagsOpen(true)}
1112
+ >
1113
+ <SlidersHorizontal className="size-3.5" />
1114
+ {t('customers.personTags.settingsButton', 'Tag settings')}
1115
+ </Button>
1116
+ <IconButton
1117
+ type="button"
1118
+ variant="outline"
1119
+ size="xs"
1120
+ className="size-7 rounded-sm border-border bg-background"
1121
+ onClick={onClose}
1122
+ >
1123
+ <X className="size-3.5" />
1124
+ </IconButton>
1125
+ </div>
1126
+ </div>
1127
+
1128
+ <div className="min-h-0 flex-1 overflow-hidden bg-background">
1129
+ {loading ? (
1130
+ <div className="flex h-full items-center justify-center px-6 py-8 text-center text-sm text-muted-foreground">
1131
+ {t('customers.personTags.loading', 'Loading...')}
1132
+ </div>
1133
+ ) : (
1134
+ <div className="flex h-full flex-col gap-4 px-5 py-4 md:flex-row">
1135
+ <div className="flex gap-2 overflow-x-auto pb-1 md:w-[220px] md:shrink-0 md:flex-col md:overflow-x-visible md:pb-0">
1136
+ {categories.map((category) => {
1137
+ const count = selectedValues[category.kind]?.size ?? 0
1138
+ const isActive = activeCategory?.kind === category.kind
1139
+ return (
1140
+ <Button
1141
+ key={category.kind}
1142
+ type="button"
1143
+ variant={isActive ? 'secondary' : 'ghost'}
1144
+ size="sm"
1145
+ className={cn(
1146
+ 'h-auto min-w-[140px] justify-between rounded-lg px-3 py-2 text-left md:w-full',
1147
+ isActive ? 'border border-border bg-muted text-foreground' : 'border border-transparent text-muted-foreground',
1148
+ )}
1149
+ onClick={() => setActiveCategoryKind(category.kind)}
1150
+ >
1151
+ <span className="truncate text-xs font-medium">
1152
+ {category.label}
1153
+ </span>
1154
+ <span className="ml-3 shrink-0 rounded-full bg-background px-2 py-0.5 text-xs font-semibold text-muted-foreground">
1155
+ {count}
1156
+ </span>
1157
+ </Button>
1158
+ )
1159
+ })}
1160
+ </div>
1161
+
1162
+ <div className="min-h-0 flex-1 overflow-y-auto rounded-xl border border-border bg-card">
1163
+ {activeCategory ? (
1164
+ <div className="flex min-h-full flex-col">
1165
+ <div className="border-b border-border px-4 py-4">
1166
+ <div className="flex flex-wrap items-start justify-between gap-3">
1167
+ <div className="space-y-1">
1168
+ <h3 className="text-sm font-semibold text-foreground">
1169
+ {activeCategory.label}
1170
+ </h3>
1171
+ <p className="max-w-[520px] text-xs leading-5 text-muted-foreground">
1172
+ {activeCategory.description}
1173
+ </p>
1174
+ </div>
1175
+ <div className="shrink-0 rounded-full border border-border bg-background px-3 py-1 text-xs font-medium text-muted-foreground">
1176
+ {t('customers.personTags.activeCount', '{{count}} selected', {
1177
+ count: activeSelection.size,
1178
+ })}
1179
+ </div>
1180
+ </div>
1181
+ </div>
1182
+
1183
+ <div className="flex-1 space-y-4 overflow-y-auto px-4 py-4">
1184
+ <div className="flex items-center gap-2 rounded-lg border border-input bg-background px-3 py-2">
1185
+ <Search className="size-3.5 shrink-0 text-muted-foreground" />
1186
+ <input
1187
+ type="text"
1188
+ value={searchValue}
1189
+ onChange={(event) => setSearchValue(event.target.value)}
1190
+ placeholder={t(
1191
+ 'customers.personTags.searchPlaceholder',
1192
+ 'Search {{category}}...',
1193
+ { category: activeCategory.label.toLowerCase() },
1194
+ )}
1195
+ className="flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
1196
+ />
1197
+ </div>
1198
+
1199
+ {filteredEntries.length > 0 ? (
1200
+ <div className="space-y-3">
1201
+ <div className="flex flex-wrap gap-x-1.5 gap-y-2">
1202
+ {filteredEntries.map((entry) => (
1203
+ <TagChip
1204
+ key={`${activeCategory.kind}:${entry.value}`}
1205
+ label={entry.label}
1206
+ color={entry.color}
1207
+ active={activeSelection.has(entry.value)}
1208
+ showColorDot={activeCategory.hasColorDots}
1209
+ onClick={() =>
1210
+ toggleValue(
1211
+ activeCategory.kind,
1212
+ entry.value,
1213
+ activeCategory.selectionMode,
1214
+ )
1215
+ }
1216
+ />
1217
+ ))}
1218
+ </div>
1219
+ {activeCategoryLoading ? (
1220
+ <div className="text-xs text-muted-foreground">
1221
+ {t('customers.personTags.loading', 'Loading...')}
1222
+ </div>
1223
+ ) : null}
1224
+ {(activeCategory.source === 'tags' || activeCategory.source === 'labels') && activeCategoryPage < activeCategoryTotalPages ? (
1225
+ <Button
1226
+ type="button"
1227
+ variant="outline"
1228
+ size="sm"
1229
+ className="rounded-lg px-3 text-xs"
1230
+ onClick={() => setActiveCategoryPage((current) => current + 1)}
1231
+ >
1232
+ {t('customers.activities.loadMore', 'Load more')}
1233
+ </Button>
1234
+ ) : null}
1235
+ </div>
1236
+ ) : activeCategoryLoading ? (
1237
+ <div className="rounded-lg border border-dashed border-border bg-background px-4 py-6 text-center text-sm text-muted-foreground">
1238
+ {t('customers.personTags.loading', 'Loading...')}
1239
+ </div>
1240
+ ) : (
1241
+ <div className="rounded-lg border border-dashed border-border bg-background px-4 py-6 text-center text-sm text-muted-foreground">
1242
+ {searchValue.trim().length > 0
1243
+ ? t('customers.personTags.emptySearchResults', 'No options match the current search.')
1244
+ : activeCategory.source === 'dictionary'
1245
+ ? t('customers.personTags.emptyDictionaryCategory', 'No options are configured for this category yet.')
1246
+ : t('customers.personTags.emptyCategory', 'No items have been added for this category yet.')}
1247
+ </div>
1248
+ )}
1249
+
1250
+ {activeCategory.supportsCreate ? (
1251
+ <div>
1252
+ {newEntryInputByKind[activeCategory.kind] !== null ? (
1253
+ <div className="inline-flex items-center rounded-full border border-dashed border-status-success-border bg-status-success-bg/70 px-2.5 py-1">
1254
+ <input
1255
+ type="text"
1256
+ autoFocus
1257
+ value={newEntryInputByKind[activeCategory.kind] ?? ''}
1258
+ disabled={creatingKind === activeCategory.kind}
1259
+ onChange={(event) =>
1260
+ setNewEntryInputByKind((previous) => ({
1261
+ ...previous,
1262
+ [activeCategory.kind]: event.target.value,
1263
+ }))
1264
+ }
1265
+ onKeyDown={(event) => {
1266
+ if (event.key === 'Enter') {
1267
+ event.preventDefault()
1268
+ void handleCreateEntry()
1269
+ }
1270
+ if (event.key === 'Escape') {
1271
+ setNewEntryInputByKind((previous) => ({
1272
+ ...previous,
1273
+ [activeCategory.kind]: null,
1274
+ }))
1275
+ }
1276
+ }}
1277
+ onBlur={() => {
1278
+ const value = newEntryInputByKind[activeCategory.kind] ?? ''
1279
+ if (creationInFlightRef.current === activeCategory.kind) {
1280
+ return
1281
+ }
1282
+ if (value.trim()) {
1283
+ void handleCreateEntry()
1284
+ } else {
1285
+ setNewEntryInputByKind((previous) => ({
1286
+ ...previous,
1287
+ [activeCategory.kind]: null,
1288
+ }))
1289
+ }
1290
+ }}
1291
+ placeholder={
1292
+ activeCategory.kind === 'tags'
1293
+ ? t('customers.people.detail.tags.placeholder', 'Type to add tags')
1294
+ : t('customers.personTags.newLabelPlaceholder', 'Label name...')
1295
+ }
1296
+ className="w-[150px] bg-transparent text-xs font-semibold text-status-success-text outline-none placeholder:text-status-success-text/60 disabled:cursor-wait disabled:opacity-70"
1297
+ />
1298
+ </div>
1299
+ ) : (
1300
+ <Button
1301
+ type="button"
1302
+ variant="ghost"
1303
+ size="sm"
1304
+ disabled={creatingKind === activeCategory.kind}
1305
+ onClick={() =>
1306
+ setNewEntryInputByKind((previous) => ({
1307
+ ...previous,
1308
+ [activeCategory.kind]: '',
1309
+ }))
1310
+ }
1311
+ className="inline-flex h-auto items-center gap-1 rounded-full border border-dashed border-status-success-border bg-transparent px-2.5 py-1.5 font-semibold text-status-success-text hover:bg-status-success-bg disabled:opacity-60"
1312
+ >
1313
+ <Plus className="size-2.5" />
1314
+ <span className="text-xs">
1315
+ {activeCategory.kind === 'tags'
1316
+ ? t('customers.personTags.newTag', 'New tag')
1317
+ : t('customers.personTags.newLabel', 'New label')}
1318
+ </span>
1319
+ </Button>
1320
+ )}
1321
+ </div>
1322
+ ) : null}
1323
+ </div>
1324
+ </div>
1325
+ ) : (
1326
+ <div className="flex h-full items-center justify-center px-6 py-8 text-center text-sm text-muted-foreground">
1327
+ {t('customers.personTags.emptyCategory', 'No items have been added for this category yet.')}
1328
+ </div>
1329
+ )}
1330
+ </div>
1331
+ </div>
1332
+ )}
1333
+ </div>
1334
+
1335
+ <div className="flex shrink-0 items-center justify-between border-t border-border bg-muted/20 px-5 py-3.5">
1336
+ <span className="text-xs text-muted-foreground">
1337
+ {t('customers.personTags.activeCount', '{{count}} selected', { count: activeCount })}
1338
+ </span>
1339
+ <div className="flex items-center gap-6">
1340
+ <Button
1341
+ type="button"
1342
+ variant="outline"
1343
+ onClick={onClose}
1344
+ className="rounded-md border-border bg-background px-4 py-2 text-sm font-semibold text-foreground"
1345
+ >
1346
+ {t('customers.personTags.cancel', 'Cancel')}
1347
+ </Button>
1348
+ <Button
1349
+ type="button"
1350
+ onClick={() => { void handleSave() }}
1351
+ disabled={saving || !hasChanges}
1352
+ className="rounded-md bg-foreground px-4 py-2 text-sm font-semibold text-background hover:bg-foreground/90"
1353
+ >
1354
+ <Check className="mr-2 size-3.5" />
1355
+ {saving
1356
+ ? t('customers.personTags.saving', 'Saving...')
1357
+ : t('customers.personTags.save', 'Save')}
1358
+ </Button>
1359
+ </div>
1360
+ </div>
1361
+ </DialogContent>
1362
+
1363
+ <ManageTagsDialog
1364
+ open={manageTagsOpen}
1365
+ onClose={() => {
1366
+ setManageTagsOpen(false)
1367
+ void loadData()
1368
+ }}
1369
+ />
1370
+ </Dialog>
1371
+ )
1372
+ }