@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,1331 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import {
5
+ CalendarDays,
6
+ ChevronsLeft,
7
+ ChevronsRight,
8
+ ChevronDown,
9
+ ChevronUp,
10
+ Check,
11
+ GripVertical,
12
+ Hash,
13
+ Info,
14
+ Plus,
15
+ Radio,
16
+ RefreshCw,
17
+ Save,
18
+ Search,
19
+ Tag,
20
+ Thermometer,
21
+ Trash2,
22
+ Users,
23
+ X,
24
+ } from 'lucide-react'
25
+ import {
26
+ DndContext,
27
+ closestCenter,
28
+ PointerSensor,
29
+ KeyboardSensor,
30
+ useSensor,
31
+ useSensors,
32
+ type DragEndEvent,
33
+ } from '@dnd-kit/core'
34
+ import {
35
+ SortableContext,
36
+ verticalListSortingStrategy,
37
+ useSortable,
38
+ arrayMove,
39
+ } from '@dnd-kit/sortable'
40
+ import { CSS } from '@dnd-kit/utilities'
41
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
42
+ import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
43
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
44
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
45
+ import { Button } from '@open-mercato/ui/primitives/button'
46
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
47
+ import {
48
+ Dialog,
49
+ DialogContent,
50
+ DialogTitle,
51
+ } from '@open-mercato/ui/primitives/dialog'
52
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Types
56
+ // ---------------------------------------------------------------------------
57
+
58
+ type TagEntryDraft = {
59
+ localId: string
60
+ id: string | null
61
+ value: string
62
+ label: string
63
+ color: string
64
+ icon: string
65
+ isDefault: boolean
66
+ isInherited: boolean
67
+ manualValue: boolean
68
+ deleted: boolean
69
+ }
70
+
71
+ type KindSetting = {
72
+ kind: string
73
+ selectionMode: 'single' | 'multi'
74
+ visibleInTags: boolean
75
+ sortOrder: number
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Category definitions — maps to /api/customers/dictionaries/{kind}
80
+ // ---------------------------------------------------------------------------
81
+
82
+ type CategoryDef = {
83
+ kind: string // route param for /api/customers/dictionaries/{kind}
84
+ icon: React.ComponentType<{ className?: string }>
85
+ shortLabelKey: string
86
+ shortLabelFallback: string
87
+ descriptionKey: string
88
+ descriptionFallback: string
89
+ badges?: string[]
90
+ noteTitleKey: string
91
+ noteTitleFallback: string
92
+ noteDescriptionKey: string
93
+ noteDescriptionFallback: string
94
+ isCustom?: boolean
95
+ }
96
+
97
+ const BUILTIN_CATEGORIES: CategoryDef[] = [
98
+ {
99
+ kind: 'statuses',
100
+ icon: Tag,
101
+ shortLabelKey: 'customers.personTags.category.statuses',
102
+ shortLabelFallback: 'Status',
103
+ descriptionKey: 'customers.tags.manage.description.customers.status',
104
+ descriptionFallback: 'Single-select values visible on the hero area of person, company, and deal cards.',
105
+ badges: ['system', 'required'],
106
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.status',
107
+ noteTitleFallback: 'System category',
108
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.status',
109
+ noteDescriptionFallback:
110
+ 'Status is required on customer cards. Existing rows can be edited, but this category should remain available tenant-wide.',
111
+ },
112
+ {
113
+ kind: 'lifecycle-stages',
114
+ icon: RefreshCw,
115
+ shortLabelKey: 'customers.personTags.category.lifecycle-stages',
116
+ shortLabelFallback: 'Lifecycle',
117
+ descriptionKey: 'customers.tags.manage.description.customers.lifecycle_stage',
118
+ descriptionFallback: 'Pipeline-aligned lifecycle values shared across CRM detail pages.',
119
+ badges: ['system'],
120
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.lifecycle_stage',
121
+ noteTitleFallback: 'Shared lifecycle values',
122
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.lifecycle_stage',
123
+ noteDescriptionFallback:
124
+ 'Use lifecycle stages to keep person and company headers visually consistent across CRM detail views.',
125
+ },
126
+ {
127
+ kind: 'sources',
128
+ icon: Radio,
129
+ shortLabelKey: 'customers.personTags.category.sources',
130
+ shortLabelFallback: 'Source',
131
+ descriptionKey: 'customers.tags.manage.description.customers.source',
132
+ descriptionFallback: 'Acquisition source labels used in Zone 1 forms and CRM summary badges.',
133
+ badges: ['system'],
134
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.source',
135
+ noteTitleFallback: 'Source dictionary',
136
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.source',
137
+ noteDescriptionFallback: 'These values are reused by customer forms and reporting filters.',
138
+ },
139
+ {
140
+ kind: 'temperature',
141
+ icon: Thermometer,
142
+ shortLabelKey: 'customers.personTags.category.temperature',
143
+ shortLabelFallback: 'Temperature',
144
+ descriptionKey: 'customers.tags.manage.description.customers.temperature',
145
+ descriptionFallback: 'Temperature / interest level for leads and contacts.',
146
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.temperature',
147
+ noteTitleFallback: 'Temperature / Interest',
148
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.temperature',
149
+ noteDescriptionFallback: 'Use temperature to quickly classify contact interest level from hot to cold.',
150
+ },
151
+ {
152
+ kind: 'renewal-quarters',
153
+ icon: CalendarDays,
154
+ shortLabelKey: 'customers.personTags.category.renewal-quarters',
155
+ shortLabelFallback: 'Renewal',
156
+ descriptionKey: 'customers.tags.manage.description.customers.renewal_quarter',
157
+ descriptionFallback: 'Renewal quarter labels for tracking contract renewal timing.',
158
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.renewal_quarter',
159
+ noteTitleFallback: 'Renewal quarter',
160
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.renewal_quarter',
161
+ noteDescriptionFallback: 'Assign renewal quarters to track when contracts or subscriptions are up for renewal.',
162
+ },
163
+ {
164
+ kind: 'person-company-roles',
165
+ icon: Users,
166
+ shortLabelKey: 'customers.personTags.category.person-company-roles',
167
+ shortLabelFallback: 'Roles',
168
+ descriptionKey: 'customers.tags.manage.description.customers.person_company_role',
169
+ descriptionFallback: 'Person-company relationship roles such as decision maker, influencer, or budget holder.',
170
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.person_company_role',
171
+ noteTitleFallback: 'Person-company roles',
172
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.person_company_role',
173
+ noteDescriptionFallback: 'Use roles to classify how a person relates to a company, for example decision maker, technical evaluator, or primary contact.',
174
+ },
175
+ {
176
+ kind: 'activity-types',
177
+ icon: CalendarDays,
178
+ shortLabelKey: 'customers.personTags.category.activity-types',
179
+ shortLabelFallback: 'Activity',
180
+ descriptionKey: 'customers.tags.manage.description.customers.activity_type',
181
+ descriptionFallback: 'Activity types for calls, emails, meetings, and other CRM interactions.',
182
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.activity_type',
183
+ noteTitleFallback: 'Activity types',
184
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.activity_type',
185
+ noteDescriptionFallback: 'Keep activity type names consistent so timeline filters remain readable across CRM views.',
186
+ },
187
+ {
188
+ kind: 'deal-statuses',
189
+ icon: Tag,
190
+ shortLabelKey: 'customers.personTags.category.deal-statuses',
191
+ shortLabelFallback: 'Deal status',
192
+ descriptionKey: 'customers.tags.manage.description.customers.deal_status',
193
+ descriptionFallback: 'Deal status labels used in pipeline views and deal detail cards.',
194
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.deal_status',
195
+ noteTitleFallback: 'Deal statuses',
196
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.deal_status',
197
+ noteDescriptionFallback: 'Deal status values affect pipeline filtering and reporting groupings.',
198
+ },
199
+ {
200
+ kind: 'industries',
201
+ icon: Hash,
202
+ shortLabelKey: 'customers.personTags.category.industries',
203
+ shortLabelFallback: 'Industry',
204
+ descriptionKey: 'customers.config.dictionaries.sections.industries.description',
205
+ descriptionFallback: 'Industry classification labels for companies and contacts.',
206
+ noteTitleKey: 'customers.tags.manage.noteTitle.customers.industry',
207
+ noteTitleFallback: 'Industry labels',
208
+ noteDescriptionKey: 'customers.tags.manage.noteDescription.customers.industry',
209
+ noteDescriptionFallback: 'Add industry categories that match your target market segments for consistent CRM classification.',
210
+ },
211
+ ]
212
+
213
+ const BUILTIN_CATEGORY_KINDS = new Set(BUILTIN_CATEGORIES.map((category) => category.kind))
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // Helpers
217
+ // ---------------------------------------------------------------------------
218
+
219
+ function createLocalId(): string {
220
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
221
+ return crypto.randomUUID()
222
+ }
223
+ return `draft-${Math.random().toString(36).slice(2)}`
224
+ }
225
+
226
+ function normalizeColor(value: string | null | undefined): string {
227
+ if (typeof value !== 'string') return '#D1D5DB'
228
+ const trimmed = value.trim()
229
+ if (/^#([0-9a-fA-F]{6})$/.test(trimmed)) {
230
+ return `#${trimmed.slice(1).toLowerCase()}`
231
+ }
232
+ return '#D1D5DB'
233
+ }
234
+
235
+ function slugifyLabel(value: string): string {
236
+ return value
237
+ .trim()
238
+ .toLowerCase()
239
+ .replace(/[^a-z0-9]+/g, '-')
240
+ .replace(/^-+|-+$/g, '')
241
+ }
242
+
243
+ function sanitizeIcon(value: string | null | undefined): string {
244
+ return typeof value === 'string' ? value.trim() : ''
245
+ }
246
+
247
+ function humanizeCategoryKind(kind: string): string {
248
+ return kind
249
+ .split(/[-_]+/)
250
+ .filter((part) => part.trim().length > 0)
251
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
252
+ .join(' ')
253
+ }
254
+
255
+ function createCustomCategoryDef(kind: string): CategoryDef {
256
+ const label = humanizeCategoryKind(kind)
257
+ return {
258
+ kind,
259
+ icon: Hash,
260
+ shortLabelKey: '',
261
+ shortLabelFallback: label,
262
+ descriptionKey: 'customers.tags.manage.customCategoryDescription',
263
+ descriptionFallback: `Custom CRM category: ${label}.`,
264
+ noteTitleKey: 'customers.tags.manage.customCategoryNoteTitle',
265
+ noteTitleFallback: 'Custom category',
266
+ noteDescriptionKey: 'customers.tags.manage.customCategoryNoteDescription',
267
+ noteDescriptionFallback: 'Use this category to group and manage additional CRM values for your team.',
268
+ isCustom: true,
269
+ }
270
+ }
271
+
272
+ function makeDraftEntry(entry: Record<string, unknown>): TagEntryDraft | null {
273
+ const id = typeof entry.id === 'string' ? entry.id : null
274
+ const value = typeof entry.value === 'string' ? entry.value.trim() : ''
275
+ if (!value.length) return null
276
+ return {
277
+ localId: id ?? createLocalId(),
278
+ id,
279
+ value,
280
+ label:
281
+ typeof entry.label === 'string' && entry.label.trim().length
282
+ ? entry.label.trim()
283
+ : value,
284
+ color: normalizeColor(typeof entry.color === 'string' ? entry.color : null),
285
+ icon: sanitizeIcon(typeof entry.icon === 'string' ? entry.icon : null),
286
+ isDefault: false,
287
+ isInherited: typeof entry.isInherited === 'boolean' ? entry.isInherited : false,
288
+ manualValue: true,
289
+ deleted: false,
290
+ }
291
+ }
292
+
293
+ function cloneDrafts(entries: TagEntryDraft[]): TagEntryDraft[] {
294
+ return entries.map((entry) => ({ ...entry }))
295
+ }
296
+
297
+ function serializeEntries(entries: TagEntryDraft[]): string {
298
+ return JSON.stringify(
299
+ entries
300
+ .filter((entry) => !entry.deleted)
301
+ .map((entry) => ({
302
+ id: entry.id,
303
+ value: entry.value,
304
+ label: entry.label,
305
+ color: entry.color,
306
+ icon: entry.icon,
307
+ })),
308
+ )
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Sortable entry row
313
+ // ---------------------------------------------------------------------------
314
+
315
+ function SortableEntryRow({
316
+ entry,
317
+ index,
318
+ total,
319
+ isDefault,
320
+ onLabelChange,
321
+ onValueChange,
322
+ onColorChange,
323
+ onDelete,
324
+ onMoveUp,
325
+ onMoveDown,
326
+ t,
327
+ }: {
328
+ entry: TagEntryDraft
329
+ index: number
330
+ total: number
331
+ isDefault: boolean
332
+ onLabelChange: (value: string) => void
333
+ onValueChange: (value: string) => void
334
+ onColorChange: (value: string) => void
335
+ onDelete: () => void
336
+ onMoveUp: () => void
337
+ onMoveDown: () => void
338
+ t: ReturnType<typeof useT>
339
+ }) {
340
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
341
+ id: entry.localId,
342
+ })
343
+ const style: React.CSSProperties = {
344
+ transform: CSS.Transform.toString(transform),
345
+ transition,
346
+ opacity: isDragging ? 0.5 : 1,
347
+ }
348
+
349
+ const isFirst = index === 0
350
+ const isLast = index === total - 1
351
+
352
+ return (
353
+ <div
354
+ ref={setNodeRef}
355
+ style={style}
356
+ className="flex items-center gap-2.5 rounded-md border border-border bg-card px-2.5 py-2"
357
+ >
358
+ {/* Grip handle */}
359
+ <div
360
+ aria-label={t('customers.tags.manage.dragHandle', 'Drag to reorder')}
361
+ title={t('customers.tags.manage.dragHandle', 'Drag to reorder')}
362
+ className="flex size-5 shrink-0 cursor-grab items-center justify-center text-muted-foreground/70"
363
+ {...attributes}
364
+ {...listeners}
365
+ >
366
+ <GripVertical className="size-3.5" />
367
+ </div>
368
+
369
+ {/* Arrow reorder buttons — additive alternative to drag */}
370
+ <div className="flex shrink-0 flex-col gap-0.5">
371
+ <IconButton
372
+ type="button"
373
+ variant="ghost"
374
+ size="xs"
375
+ aria-label={t('customers.tags.manage.moveUp', 'Move up')}
376
+ disabled={isFirst}
377
+ onClick={onMoveUp}
378
+ className="h-5 w-5 text-muted-foreground hover:text-foreground disabled:opacity-40"
379
+ >
380
+ <ChevronUp className="size-3" />
381
+ </IconButton>
382
+ <IconButton
383
+ type="button"
384
+ variant="ghost"
385
+ size="xs"
386
+ aria-label={t('customers.tags.manage.moveDown', 'Move down')}
387
+ disabled={isLast}
388
+ onClick={onMoveDown}
389
+ className="h-5 w-5 text-muted-foreground hover:text-foreground disabled:opacity-40"
390
+ >
391
+ <ChevronDown className="size-3" />
392
+ </IconButton>
393
+ </div>
394
+
395
+ {/* Label + default indicator */}
396
+ <div className="flex min-w-0 flex-1 flex-col gap-1">
397
+ <input
398
+ type="text"
399
+ value={entry.label}
400
+ onChange={(e) => onLabelChange(e.target.value)}
401
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm font-medium text-foreground outline-none focus:border-foreground"
402
+ />
403
+ {isDefault && (
404
+ <div className="flex items-center gap-1.5">
405
+ <Check className="size-2.5 text-status-success-icon" />
406
+ <span className="text-xs text-muted-foreground">
407
+ {t('customers.tags.manage.defaultEntry', 'default when creating new records')}
408
+ </span>
409
+ </div>
410
+ )}
411
+ </div>
412
+
413
+ {/* Slug (read-only style) */}
414
+ <div className="w-[140px] shrink-0">
415
+ <input
416
+ type="text"
417
+ value={entry.value}
418
+ onChange={(e) => onValueChange(slugifyLabel(e.target.value))}
419
+ className="w-full rounded-md bg-muted px-2.5 py-2 text-xs font-medium text-muted-foreground outline-none"
420
+ />
421
+ </div>
422
+
423
+ {/* Color picker */}
424
+ <div className="flex w-[80px] shrink-0 items-center gap-1.5 rounded-md border border-input px-2 py-1.5">
425
+ <label className="relative size-4 shrink-0 cursor-pointer">
426
+ <span
427
+ className="block size-full rounded-sm"
428
+ style={{ backgroundColor: normalizeColor(entry.color) }}
429
+ />
430
+ <input
431
+ type="color"
432
+ value={normalizeColor(entry.color)}
433
+ onChange={(e) => onColorChange(normalizeColor(e.target.value))}
434
+ className="absolute inset-0 size-full cursor-pointer opacity-0"
435
+ />
436
+ </label>
437
+ <span className="text-xs font-medium text-muted-foreground">
438
+ {normalizeColor(entry.color)}
439
+ </span>
440
+ </div>
441
+
442
+ {/* Delete */}
443
+ <IconButton
444
+ type="button"
445
+ variant="ghost"
446
+ size="sm"
447
+ className="size-8 shrink-0 text-muted-foreground hover:text-destructive"
448
+ onClick={onDelete}
449
+ aria-label={t('customers.tags.manage.delete', 'Delete')}
450
+ >
451
+ <Trash2 className="size-3.5" />
452
+ </IconButton>
453
+ </div>
454
+ )
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // Main dialog
459
+ // ---------------------------------------------------------------------------
460
+
461
+ interface ManageTagsDialogProps {
462
+ open: boolean
463
+ onClose: () => void
464
+ }
465
+
466
+ export function ManageTagsDialog({ open, onClose }: ManageTagsDialogProps) {
467
+ const t = useT()
468
+ const [categories, setCategories] = React.useState<CategoryDef[]>(BUILTIN_CATEGORIES)
469
+ const [activeTab, setActiveTab] = React.useState(BUILTIN_CATEGORIES[0].kind)
470
+ const [loading, setLoading] = React.useState(true)
471
+ const [saving, setSaving] = React.useState(false)
472
+ const [searchValue, setSearchValue] = React.useState('')
473
+ const [entryCounts, setEntryCounts] = React.useState<Record<string, number>>({})
474
+ const [draftsByKind, setDraftsByKind] = React.useState<Record<string, TagEntryDraft[]>>({})
475
+ const [originalByKind, setOriginalByKind] = React.useState<Record<string, TagEntryDraft[]>>({})
476
+ const [createCategoryOpen, setCreateCategoryOpen] = React.useState(false)
477
+ const [newCategoryName, setNewCategoryName] = React.useState('')
478
+ const [newCategorySelectionMode, setNewCategorySelectionMode] = React.useState<'single' | 'multi'>('multi')
479
+ const [creatingCategory, setCreatingCategory] = React.useState(false)
480
+ const [canScrollLeft, setCanScrollLeft] = React.useState(false)
481
+ const [canScrollRight, setCanScrollRight] = React.useState(false)
482
+ const categoryRailRef = React.useRef<HTMLDivElement | null>(null)
483
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
484
+ formId: string
485
+ resourceKind: string
486
+ resourceId: string
487
+ retryLastMutation: () => Promise<boolean>
488
+ }>({
489
+ contextId: 'customers-manage-tags',
490
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
491
+ })
492
+ const mutationContext = React.useMemo(
493
+ () => ({
494
+ formId: 'customers-manage-tags',
495
+ resourceKind: 'customers.dictionary_entry',
496
+ resourceId: 'bulk',
497
+ retryLastMutation,
498
+ }),
499
+ [retryLastMutation],
500
+ )
501
+ const runGuardedMutation = React.useCallback(
502
+ async <T,>(operation: () => Promise<T>, mutationPayload: Record<string, unknown>) =>
503
+ runMutation({
504
+ operation,
505
+ mutationPayload,
506
+ context: mutationContext,
507
+ }),
508
+ [mutationContext, runMutation],
509
+ )
510
+ const translatedCategories = React.useMemo(
511
+ () =>
512
+ categories.map((category) => ({
513
+ ...category,
514
+ shortLabel: category.shortLabelKey
515
+ ? t(category.shortLabelKey, category.shortLabelFallback)
516
+ : category.shortLabelFallback,
517
+ description: category.descriptionKey
518
+ ? t(
519
+ category.descriptionKey,
520
+ category.descriptionFallback,
521
+ category.isCustom ? { name: category.shortLabelFallback } : undefined,
522
+ )
523
+ : category.descriptionFallback,
524
+ noteTitle: category.noteTitleKey
525
+ ? t(category.noteTitleKey, category.noteTitleFallback)
526
+ : category.noteTitleFallback,
527
+ noteDescription: category.noteDescriptionKey
528
+ ? t(category.noteDescriptionKey, category.noteDescriptionFallback)
529
+ : category.noteDescriptionFallback,
530
+ })),
531
+ [categories, t],
532
+ )
533
+
534
+ const sensors = useSensors(
535
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
536
+ useSensor(KeyboardSensor),
537
+ )
538
+
539
+ // --- data loading ---
540
+
541
+ const loadData = React.useCallback(async () => {
542
+ setLoading(true)
543
+ try {
544
+ let kindSettings: KindSetting[] = []
545
+ try {
546
+ const settings = await readApiResultOrThrow<{ items?: KindSetting[] }>(
547
+ '/api/customers/dictionaries/kind-settings',
548
+ { cache: 'no-store' },
549
+ )
550
+ kindSettings = Array.isArray(settings?.items) ? settings.items : []
551
+ } catch {
552
+ kindSettings = []
553
+ }
554
+
555
+ const customCategories = kindSettings
556
+ .filter((setting) => !BUILTIN_CATEGORY_KINDS.has(setting.kind))
557
+ .sort((left, right) => left.sortOrder - right.sortOrder || left.kind.localeCompare(right.kind))
558
+ .map((setting) => createCustomCategoryDef(setting.kind))
559
+ const resolvedCategories = [...BUILTIN_CATEGORIES, ...customCategories]
560
+
561
+ const loadedDrafts: Record<string, TagEntryDraft[]> = {}
562
+ const counts: Record<string, number> = {}
563
+
564
+ for (const category of resolvedCategories) {
565
+ try {
566
+ const data = await readApiResultOrThrow<{
567
+ items?: Array<Record<string, unknown>>
568
+ }>(`/api/customers/dictionaries/${category.kind}`, { cache: 'no-store' })
569
+ const entries = Array.isArray(data?.items)
570
+ ? data.items
571
+ .map(makeDraftEntry)
572
+ .filter((entry): entry is TagEntryDraft => entry !== null)
573
+ : []
574
+ loadedDrafts[category.kind] = entries
575
+ counts[category.kind] = entries.length
576
+ } catch {
577
+ loadedDrafts[category.kind] = []
578
+ counts[category.kind] = 0
579
+ }
580
+ }
581
+
582
+ setCategories(resolvedCategories)
583
+ setDraftsByKind(
584
+ Object.fromEntries(
585
+ Object.entries(loadedDrafts).map(([k, v]) => [k, cloneDrafts(v)]),
586
+ ),
587
+ )
588
+ setOriginalByKind(
589
+ Object.fromEntries(
590
+ Object.entries(loadedDrafts).map(([k, v]) => [k, cloneDrafts(v)]),
591
+ ),
592
+ )
593
+ setEntryCounts(counts)
594
+ setActiveTab((current) =>
595
+ resolvedCategories.some((category) => category.kind === current)
596
+ ? current
597
+ : resolvedCategories[0]?.kind ?? BUILTIN_CATEGORIES[0].kind,
598
+ )
599
+ } catch (error) {
600
+ const message =
601
+ error instanceof Error
602
+ ? error.message
603
+ : t('customers.tags.manage.loadError', 'Failed to load tag dictionaries.')
604
+ flash(message, 'error')
605
+ } finally {
606
+ setLoading(false)
607
+ }
608
+ }, [t])
609
+
610
+ React.useEffect(() => {
611
+ if (!open) return
612
+ setSearchValue('')
613
+ setCreateCategoryOpen(false)
614
+ setNewCategoryName('')
615
+ setNewCategorySelectionMode('multi')
616
+ loadData().catch((err) => console.warn('[ManageTagsDialog] loadData failed', err))
617
+ }, [loadData, open])
618
+
619
+ // --- derived state ---
620
+
621
+ const activeMeta = translatedCategories.find((category) => category.kind === activeTab) ?? null
622
+ const activeEntries = draftsByKind[activeTab] ?? []
623
+ const visibleEntries = React.useMemo(() => {
624
+ const query = searchValue.trim().toLowerCase()
625
+ return activeEntries.filter((entry) => {
626
+ if (entry.deleted) return false
627
+ if (!query) return true
628
+ return (
629
+ entry.label.toLowerCase().includes(query) || entry.value.toLowerCase().includes(query)
630
+ )
631
+ })
632
+ }, [activeEntries, searchValue])
633
+
634
+ const hasChanges = React.useMemo(
635
+ () =>
636
+ categories.some((category) => {
637
+ const original = originalByKind[category.kind] ?? []
638
+ const current = draftsByKind[category.kind] ?? []
639
+ return serializeEntries(original) !== serializeEntries(current)
640
+ }),
641
+ [categories, draftsByKind, originalByKind],
642
+ )
643
+
644
+ // --- draft mutations ---
645
+
646
+ const updateDraftEntry = React.useCallback(
647
+ (kind: string, localId: string, updater: (e: TagEntryDraft) => TagEntryDraft) => {
648
+ setDraftsByKind((current) => ({
649
+ ...current,
650
+ [kind]: (current[kind] ?? []).map((entry) =>
651
+ entry.localId === localId ? updater(entry) : entry,
652
+ ),
653
+ }))
654
+ },
655
+ [],
656
+ )
657
+
658
+ const handleAddEntry = React.useCallback(() => {
659
+ setDraftsByKind((current) => ({
660
+ ...current,
661
+ [activeTab]: [
662
+ ...(current[activeTab] ?? []),
663
+ {
664
+ localId: createLocalId(),
665
+ id: null,
666
+ value: '',
667
+ label: '',
668
+ color: '#D1D5DB',
669
+ icon: '',
670
+ isDefault: false,
671
+ isInherited: false,
672
+ manualValue: false,
673
+ deleted: false,
674
+ },
675
+ ],
676
+ }))
677
+ }, [activeTab])
678
+
679
+ const handleDeleteEntry = React.useCallback(
680
+ (kind: string, localId: string) => {
681
+ setDraftsByKind((current) => {
682
+ const nextEntries = (current[kind] ?? [])
683
+ .map((entry) => {
684
+ if (entry.localId !== localId) return entry
685
+ if (!entry.id) return null
686
+ return { ...entry, deleted: true }
687
+ })
688
+ .filter((entry): entry is TagEntryDraft => entry !== null)
689
+ return { ...current, [kind]: nextEntries }
690
+ })
691
+ },
692
+ [],
693
+ )
694
+
695
+ // --- drag & drop ---
696
+
697
+ const handleDragEnd = React.useCallback(
698
+ (event: DragEndEvent) => {
699
+ const { active, over } = event
700
+ if (!over || active.id === over.id) return
701
+
702
+ setDraftsByKind((current) => {
703
+ const entries = current[activeTab] ?? []
704
+ const liveEntries = entries.filter((e) => !e.deleted)
705
+ const oldIndex = liveEntries.findIndex((e) => e.localId === active.id)
706
+ const newIndex = liveEntries.findIndex((e) => e.localId === over.id)
707
+ if (oldIndex === -1 || newIndex === -1) return current
708
+ const reordered = arrayMove(liveEntries, oldIndex, newIndex)
709
+ const deletedEntries = entries.filter((e) => e.deleted)
710
+ return { ...current, [activeTab]: [...reordered, ...deletedEntries] }
711
+ })
712
+ },
713
+ [activeTab],
714
+ )
715
+
716
+ const moveEntryByDelta = React.useCallback(
717
+ (localId: string, delta: -1 | 1) => {
718
+ setDraftsByKind((current) => {
719
+ const entries = current[activeTab] ?? []
720
+ const liveEntries = entries.filter((entry) => !entry.deleted)
721
+ const oldIndex = liveEntries.findIndex((entry) => entry.localId === localId)
722
+ if (oldIndex < 0) return current
723
+ const newIndex = oldIndex + delta
724
+ if (newIndex < 0 || newIndex >= liveEntries.length) return current
725
+ const reordered = arrayMove(liveEntries, oldIndex, newIndex)
726
+ const deletedEntries = entries.filter((entry) => entry.deleted)
727
+ return { ...current, [activeTab]: [...reordered, ...deletedEntries] }
728
+ })
729
+ },
730
+ [activeTab],
731
+ )
732
+
733
+ // --- save ---
734
+
735
+ const handleSave = React.useCallback(async () => {
736
+ if (saving) return
737
+
738
+ for (const category of categories) {
739
+ const entries = draftsByKind[category.kind] ?? []
740
+ for (const entry of entries) {
741
+ if (entry.deleted) continue
742
+ const nextLabel = entry.label.trim()
743
+ const nextValue = entry.value.trim()
744
+ if (!nextLabel || !nextValue) {
745
+ flash(
746
+ t(
747
+ 'customers.tags.manage.validation.required',
748
+ 'Each entry must have both a label and a slug before saving.',
749
+ ),
750
+ 'error',
751
+ )
752
+ return
753
+ }
754
+ }
755
+ }
756
+
757
+ setSaving(true)
758
+ try {
759
+ for (const category of categories) {
760
+ const currentEntries = draftsByKind[category.kind] ?? []
761
+ const originalEntries = originalByKind[category.kind] ?? []
762
+ const originalById = new Map(
763
+ originalEntries.filter((e) => e.id).map((e) => [e.id as string, e]),
764
+ )
765
+
766
+ for (const entry of currentEntries) {
767
+ if (entry.deleted) {
768
+ if (entry.id) {
769
+ await runGuardedMutation(
770
+ () =>
771
+ apiCallOrThrow(`/api/customers/dictionaries/${category.kind}/${entry.id}`, {
772
+ method: 'DELETE',
773
+ }),
774
+ { kind: category.kind, entryId: entry.id, operation: 'delete' },
775
+ )
776
+ }
777
+ continue
778
+ }
779
+
780
+ const payload: Record<string, unknown> = {
781
+ value: entry.value.trim(),
782
+ label: entry.label.trim(),
783
+ color: normalizeColor(entry.color),
784
+ icon: sanitizeIcon(entry.icon) || null,
785
+ }
786
+
787
+ if (!entry.id) {
788
+ await runGuardedMutation(
789
+ () =>
790
+ apiCallOrThrow(`/api/customers/dictionaries/${category.kind}`, {
791
+ method: 'POST',
792
+ headers: { 'content-type': 'application/json' },
793
+ body: JSON.stringify(payload),
794
+ }),
795
+ { kind: category.kind, value: payload.value, operation: 'create' },
796
+ )
797
+ continue
798
+ }
799
+
800
+ // Skip inherited entries — they belong to a parent org and can't be PATCHed
801
+ if (entry.isInherited) continue
802
+
803
+ const originalEntry = originalById.get(entry.id)
804
+ if (
805
+ originalEntry &&
806
+ originalEntry.value === (payload.value as string) &&
807
+ originalEntry.label === (payload.label as string) &&
808
+ normalizeColor(originalEntry.color) === normalizeColor(payload.color as string) &&
809
+ sanitizeIcon(originalEntry.icon) === sanitizeIcon(payload.icon as string)
810
+ ) {
811
+ continue
812
+ }
813
+
814
+ await runGuardedMutation(
815
+ () =>
816
+ apiCallOrThrow(`/api/customers/dictionaries/${category.kind}/${entry.id}`, {
817
+ method: 'PATCH',
818
+ headers: { 'content-type': 'application/json' },
819
+ body: JSON.stringify(payload),
820
+ }),
821
+ { kind: category.kind, entryId: entry.id, value: payload.value, operation: 'update' },
822
+ )
823
+ }
824
+ }
825
+
826
+ flash(t('customers.tags.manage.saveSuccess', 'Tag dictionaries updated.'), 'success')
827
+ await loadData()
828
+ } catch (error) {
829
+ const message =
830
+ error instanceof Error
831
+ ? error.message
832
+ : t('customers.tags.manage.saveError', 'Failed to save tag dictionaries.')
833
+ flash(message, 'error')
834
+ } finally {
835
+ setSaving(false)
836
+ }
837
+ }, [categories, draftsByKind, loadData, originalByKind, runGuardedMutation, saving, t])
838
+
839
+ const updateCategoryRailState = React.useCallback(() => {
840
+ const rail = categoryRailRef.current
841
+ if (!rail) {
842
+ setCanScrollLeft(false)
843
+ setCanScrollRight(false)
844
+ return
845
+ }
846
+ const maxScrollLeft = Math.max(0, rail.scrollWidth - rail.clientWidth)
847
+ setCanScrollLeft(rail.scrollLeft > 4)
848
+ setCanScrollRight(rail.scrollLeft < maxScrollLeft - 4)
849
+ }, [])
850
+
851
+ const scrollCategoryRail = React.useCallback(
852
+ (direction: 'left' | 'right') => {
853
+ const rail = categoryRailRef.current
854
+ if (!rail) return
855
+ const offset = Math.max(180, Math.round(rail.clientWidth * 0.65))
856
+ rail.scrollBy({
857
+ left: direction === 'left' ? -offset : offset,
858
+ behavior: 'smooth',
859
+ })
860
+ },
861
+ [],
862
+ )
863
+
864
+ React.useEffect(() => {
865
+ if (!open) return
866
+ const rail = categoryRailRef.current
867
+ if (!rail) return
868
+ updateCategoryRailState()
869
+ rail.addEventListener('scroll', updateCategoryRailState, { passive: true })
870
+ const resizeObserver =
871
+ typeof ResizeObserver !== 'undefined'
872
+ ? new ResizeObserver(() => updateCategoryRailState())
873
+ : null
874
+ resizeObserver?.observe(rail)
875
+ return () => {
876
+ rail.removeEventListener('scroll', updateCategoryRailState)
877
+ resizeObserver?.disconnect()
878
+ }
879
+ }, [open, translatedCategories, updateCategoryRailState])
880
+
881
+ const handleCreateCategory = React.useCallback(async () => {
882
+ if (creatingCategory) return
883
+ const trimmedName = newCategoryName.trim()
884
+ const kind = slugifyLabel(trimmedName)
885
+ if (!kind.length) {
886
+ flash(
887
+ t('customers.tags.manage.addCategoryRequired', 'Enter a category name first.'),
888
+ 'error',
889
+ )
890
+ return
891
+ }
892
+ if (categories.some((category) => category.kind === kind)) {
893
+ setActiveTab(kind)
894
+ setCreateCategoryOpen(false)
895
+ return
896
+ }
897
+
898
+ setCreatingCategory(true)
899
+ try {
900
+ await runGuardedMutation(
901
+ () =>
902
+ apiCallOrThrow('/api/customers/dictionaries/kind-settings', {
903
+ method: 'PATCH',
904
+ headers: { 'content-type': 'application/json' },
905
+ body: JSON.stringify({
906
+ kind,
907
+ selectionMode: newCategorySelectionMode,
908
+ visibleInTags: true,
909
+ sortOrder: categories.length + 1,
910
+ }),
911
+ }),
912
+ {
913
+ kind,
914
+ selectionMode: newCategorySelectionMode,
915
+ operation: 'createCategory',
916
+ },
917
+ )
918
+ await loadData()
919
+ setActiveTab(kind)
920
+ setCreateCategoryOpen(false)
921
+ setNewCategoryName('')
922
+ setNewCategorySelectionMode('multi')
923
+ flash(t('customers.tags.manage.createCategorySuccess', 'Category created.'), 'success')
924
+ } catch (error) {
925
+ const message =
926
+ error instanceof Error
927
+ ? error.message
928
+ : t('customers.tags.manage.createCategoryError', 'Failed to create category.')
929
+ flash(message, 'error')
930
+ } finally {
931
+ setCreatingCategory(false)
932
+ }
933
+ }, [categories, creatingCategory, loadData, newCategoryName, newCategorySelectionMode, runGuardedMutation, t])
934
+
935
+ // --- keyboard shortcut ---
936
+
937
+ React.useEffect(() => {
938
+ if (!open) return
939
+ const handler = (e: KeyboardEvent) => {
940
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
941
+ e.preventDefault()
942
+ void handleSave()
943
+ }
944
+ }
945
+ window.addEventListener('keydown', handler)
946
+ return () => window.removeEventListener('keydown', handler)
947
+ }, [open, handleSave])
948
+
949
+ // --- render ---
950
+
951
+ return (
952
+ <Dialog
953
+ open={open}
954
+ onOpenChange={(nextOpen) => {
955
+ if (!nextOpen) onClose()
956
+ }}
957
+ >
958
+ <DialogContent
959
+ className="flex max-h-[90vh] flex-col overflow-hidden border-border p-0 shadow-[0px_20px_48px_0px_rgba(0,0,0,0.18)] sm:max-w-[820px] sm:rounded-lg [&>[data-dialog-close]]:hidden"
960
+ aria-describedby={undefined}
961
+ >
962
+ <VisuallyHidden>
963
+ <DialogTitle>{t('customers.tags.manage.title', 'Manage tags')}</DialogTitle>
964
+ </VisuallyHidden>
965
+
966
+ {/* Header */}
967
+ <div className="flex shrink-0 items-center justify-between pb-2.5 pl-6 pr-5 pt-4">
968
+ <div className="flex flex-col gap-1">
969
+ <h2 className="text-base font-bold leading-tight text-foreground">
970
+ {t('customers.tags.manage.title', 'Manage tags')}
971
+ </h2>
972
+ <p className="text-xs leading-tight text-muted-foreground">
973
+ {t(
974
+ 'customers.tags.manage.subtitle',
975
+ 'Tag dictionaries for the entire tenant',
976
+ )}
977
+ </p>
978
+ </div>
979
+ <div className="flex items-center gap-2">
980
+ <Button
981
+ type="button"
982
+ variant="outline"
983
+ size="sm"
984
+ className="h-auto gap-1.5 rounded-md px-3 py-2 text-xs font-semibold"
985
+ onClick={() => {
986
+ setCreateCategoryOpen((current) => !current)
987
+ setNewCategoryName('')
988
+ setNewCategorySelectionMode('multi')
989
+ }}
990
+ >
991
+ <Plus className="size-3.5" />
992
+ {t('customers.tags.manage.addCategory', 'New category')}
993
+ </Button>
994
+ <IconButton
995
+ type="button"
996
+ variant="outline"
997
+ size="sm"
998
+ className="size-7 shrink-0 rounded-md border-border"
999
+ onClick={onClose}
1000
+ aria-label={t('customers.tags.manage.closeDialog', 'Close')}
1001
+ >
1002
+ <X className="size-3.5" />
1003
+ </IconButton>
1004
+ </div>
1005
+ </div>
1006
+ <div className="h-px shrink-0 bg-border" />
1007
+
1008
+ {loading ? (
1009
+ <div className="px-7 py-12 text-center text-sm text-muted-foreground">
1010
+ {t('customers.tags.manage.loading', 'Loading...')}
1011
+ </div>
1012
+ ) : (
1013
+ <>
1014
+ {createCategoryOpen ? (
1015
+ <div className="flex shrink-0 flex-wrap items-center gap-2.5 border-b border-border px-6 py-3">
1016
+ <div className="min-w-[220px] flex-1 rounded-md border border-input bg-background px-3 py-2">
1017
+ <input
1018
+ type="text"
1019
+ value={newCategoryName}
1020
+ onChange={(event) => setNewCategoryName(event.target.value)}
1021
+ onKeyDown={(event) => {
1022
+ if (event.key === 'Enter') {
1023
+ event.preventDefault()
1024
+ void handleCreateCategory()
1025
+ }
1026
+ }}
1027
+ placeholder={t('customers.tags.manage.addCategoryPlaceholder', 'Category name...')}
1028
+ className="w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
1029
+ />
1030
+ </div>
1031
+ <div className="flex items-center gap-1 rounded-md border border-border bg-muted/60 p-1">
1032
+ <Button
1033
+ type="button"
1034
+ variant={newCategorySelectionMode === 'single' ? 'secondary' : 'ghost'}
1035
+ size="sm"
1036
+ className="h-auto rounded-md px-2.5 py-1.5 text-xs"
1037
+ onClick={() => setNewCategorySelectionMode('single')}
1038
+ >
1039
+ {t('customers.tags.manage.categoryMode.single', 'Single')}
1040
+ </Button>
1041
+ <Button
1042
+ type="button"
1043
+ variant={newCategorySelectionMode === 'multi' ? 'secondary' : 'ghost'}
1044
+ size="sm"
1045
+ className="h-auto rounded-md px-2.5 py-1.5 text-xs"
1046
+ onClick={() => setNewCategorySelectionMode('multi')}
1047
+ >
1048
+ {t('customers.tags.manage.categoryMode.multi', 'Multi')}
1049
+ </Button>
1050
+ </div>
1051
+ <Button
1052
+ type="button"
1053
+ size="sm"
1054
+ className="h-auto rounded-md px-3 py-2 text-xs font-semibold"
1055
+ onClick={() => {
1056
+ void handleCreateCategory()
1057
+ }}
1058
+ disabled={creatingCategory}
1059
+ >
1060
+ {creatingCategory
1061
+ ? t('customers.tags.manage.creatingCategory', 'Creating...')
1062
+ : t('customers.tags.manage.createCategory', 'Create category')}
1063
+ </Button>
1064
+ </div>
1065
+ ) : null}
1066
+
1067
+ {/* Tab bar */}
1068
+ <div className="flex shrink-0 items-center gap-2 border-b border-border px-4 py-1.5">
1069
+ <IconButton
1070
+ type="button"
1071
+ variant="ghost"
1072
+ size="sm"
1073
+ className="size-8 shrink-0 rounded-full"
1074
+ onClick={() => scrollCategoryRail('left')}
1075
+ disabled={!canScrollLeft}
1076
+ aria-label={t('customers.tags.manage.scrollLeft', 'Scroll categories left')}
1077
+ >
1078
+ <ChevronsLeft className="size-3.5" />
1079
+ </IconButton>
1080
+ <div
1081
+ ref={categoryRailRef}
1082
+ className="min-w-0 flex-1 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
1083
+ >
1084
+ <div className="flex items-end gap-0.5">
1085
+ {translatedCategories.map((category) => {
1086
+ const Icon = category.icon
1087
+ const isActive = category.kind === activeTab
1088
+ const count = entryCounts[category.kind] ?? 0
1089
+ return (
1090
+ <Button
1091
+ key={category.kind}
1092
+ type="button"
1093
+ variant="ghost"
1094
+ size="sm"
1095
+ onClick={() => {
1096
+ setActiveTab(category.kind)
1097
+ setSearchValue('')
1098
+ }}
1099
+ className={`flex h-auto shrink-0 items-center gap-1.5 rounded-none border-b-2 px-2.5 py-2 hover:bg-transparent ${
1100
+ isActive
1101
+ ? '-mb-px border-foreground text-foreground'
1102
+ : '-mb-px border-transparent text-muted-foreground'
1103
+ }`}
1104
+ >
1105
+ <Icon className="size-3.5" />
1106
+ <span
1107
+ className={`whitespace-nowrap text-xs ${isActive ? 'font-semibold' : 'font-medium'}`}
1108
+ >
1109
+ {category.shortLabel}
1110
+ </span>
1111
+ <span className="rounded-sm bg-muted px-1 py-px text-overline font-semibold text-foreground">
1112
+ {count}
1113
+ </span>
1114
+ </Button>
1115
+ )
1116
+ })}
1117
+ </div>
1118
+ </div>
1119
+ <IconButton
1120
+ type="button"
1121
+ variant="ghost"
1122
+ size="sm"
1123
+ className="size-8 shrink-0 rounded-full"
1124
+ onClick={() => scrollCategoryRail('right')}
1125
+ disabled={!canScrollRight}
1126
+ aria-label={t('customers.tags.manage.scrollRight', 'Scroll categories right')}
1127
+ >
1128
+ <ChevronsRight className="size-3.5" />
1129
+ </IconButton>
1130
+ </div>
1131
+
1132
+ {/* Content */}
1133
+ <div className="flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto px-6 py-3.5">
1134
+ {activeMeta ? (
1135
+ <>
1136
+ {/* Category header + search */}
1137
+ <div className="flex items-center justify-between">
1138
+ <div className="flex flex-col gap-1">
1139
+ <div className="flex items-center gap-1.5">
1140
+ <span className="text-sm font-bold text-foreground">
1141
+ {activeMeta.shortLabel}
1142
+ </span>
1143
+ {(activeMeta.badges ?? []).map((badge) => (
1144
+ <span
1145
+ key={badge}
1146
+ className={`rounded-sm px-2 py-0.5 text-overline font-bold ${
1147
+ badge === 'required'
1148
+ ? 'bg-status-warning-bg text-status-warning-text'
1149
+ : 'bg-muted text-muted-foreground'
1150
+ }`}
1151
+ >
1152
+ {badge === 'required'
1153
+ ? t('customers.tags.manage.badge.required', 'REQUIRED')
1154
+ : t('customers.tags.manage.badge.system', 'SYSTEM')}
1155
+ </span>
1156
+ ))}
1157
+ </div>
1158
+ <div className="flex items-center gap-1.5">
1159
+ <Info className="size-3 shrink-0 text-muted-foreground" />
1160
+ <span className="text-xs text-muted-foreground">
1161
+ {activeMeta.description}
1162
+ </span>
1163
+ </div>
1164
+ </div>
1165
+ <div className="relative w-[220px] shrink-0">
1166
+ <Search className="absolute left-3 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground/70" />
1167
+ <input
1168
+ type="text"
1169
+ value={searchValue}
1170
+ onChange={(e) => setSearchValue(e.target.value)}
1171
+ placeholder={t('customers.tags.manage.search', 'Search values...')}
1172
+ className="w-full rounded-md border border-input bg-background py-2 pl-9 pr-3 text-xs text-foreground outline-none placeholder:text-muted-foreground/70 focus:border-foreground"
1173
+ />
1174
+ </div>
1175
+ </div>
1176
+
1177
+ {/* Column headers */}
1178
+ <div className="flex items-center gap-3 px-3 py-1.5">
1179
+ <div className="w-[18px] shrink-0" />
1180
+ <div className="min-w-0 flex-1">
1181
+ <span className="text-overline font-bold uppercase text-muted-foreground">
1182
+ {t('customers.tags.manage.columns.label', 'LABEL')}
1183
+ </span>
1184
+ </div>
1185
+ <div className="w-[140px] shrink-0">
1186
+ <span className="text-overline font-bold uppercase text-muted-foreground">
1187
+ {t('customers.tags.manage.columns.slug', 'SLUG')}
1188
+ </span>
1189
+ </div>
1190
+ <div className="w-[80px] shrink-0">
1191
+ <span className="text-overline font-bold uppercase text-muted-foreground">
1192
+ {t('customers.tags.manage.columns.color', 'COLOR')}
1193
+ </span>
1194
+ </div>
1195
+ <div className="w-[32px] shrink-0" />
1196
+ </div>
1197
+
1198
+ {/* Entry rows */}
1199
+ <div className="flex flex-col gap-2">
1200
+ <DndContext
1201
+ sensors={sensors}
1202
+ collisionDetection={closestCenter}
1203
+ onDragEnd={handleDragEnd}
1204
+ >
1205
+ <SortableContext
1206
+ items={visibleEntries.map((e) => e.localId)}
1207
+ strategy={verticalListSortingStrategy}
1208
+ >
1209
+ {visibleEntries.map((entry, index) => (
1210
+ <SortableEntryRow
1211
+ key={entry.localId}
1212
+ entry={entry}
1213
+ index={index}
1214
+ total={visibleEntries.length}
1215
+ isDefault={index === 0 && entry.id !== null}
1216
+ onLabelChange={(value) => {
1217
+ updateDraftEntry(activeTab, entry.localId, (current) => ({
1218
+ ...current,
1219
+ label: value,
1220
+ value: current.manualValue
1221
+ ? current.value
1222
+ : slugifyLabel(value),
1223
+ }))
1224
+ }}
1225
+ onValueChange={(value) => {
1226
+ updateDraftEntry(activeTab, entry.localId, (current) => ({
1227
+ ...current,
1228
+ value,
1229
+ manualValue: true,
1230
+ }))
1231
+ }}
1232
+ onColorChange={(value) => {
1233
+ updateDraftEntry(activeTab, entry.localId, (current) => ({
1234
+ ...current,
1235
+ color: value,
1236
+ }))
1237
+ }}
1238
+ onDelete={() => handleDeleteEntry(activeTab, entry.localId)}
1239
+ onMoveUp={() => moveEntryByDelta(entry.localId, -1)}
1240
+ onMoveDown={() => moveEntryByDelta(entry.localId, 1)}
1241
+ t={t}
1242
+ />
1243
+ ))}
1244
+ </SortableContext>
1245
+ </DndContext>
1246
+
1247
+ {visibleEntries.length === 0 && (
1248
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">
1249
+ {t(
1250
+ 'customers.tags.manage.noMatches',
1251
+ 'No entries match the current search.',
1252
+ )}
1253
+ </div>
1254
+ )}
1255
+ </div>
1256
+
1257
+ {/* Add new value */}
1258
+ <Button
1259
+ type="button"
1260
+ variant="ghost"
1261
+ onClick={handleAddEntry}
1262
+ className="flex h-auto w-full items-center justify-center gap-2 rounded-md border border-dashed border-border bg-background p-3 text-xs font-semibold text-foreground hover:bg-muted"
1263
+ >
1264
+ <Plus className="size-3.5" />
1265
+ {t('customers.tags.manage.addValue', 'Add new value')}
1266
+ </Button>
1267
+
1268
+ {/* Info note */}
1269
+ <div className="flex items-start gap-2.5 rounded-md bg-muted px-3.5 py-3">
1270
+ <Info className="mt-0.5 size-3.5 shrink-0 text-muted-foreground" />
1271
+ <div className="flex flex-1 flex-col gap-1">
1272
+ <span className="text-xs font-semibold text-foreground">
1273
+ {activeMeta.noteTitle}
1274
+ </span>
1275
+ <span className="text-xs text-muted-foreground">
1276
+ {activeMeta.noteDescription}
1277
+ </span>
1278
+ </div>
1279
+ </div>
1280
+ </>
1281
+ ) : (
1282
+ <div className="py-8 text-center text-sm text-muted-foreground">
1283
+ {t('customers.tags.manage.noDictionaries', 'No tag categories found.')}
1284
+ </div>
1285
+ )}
1286
+ </div>
1287
+
1288
+ {/* Separator */}
1289
+ <div className="h-px shrink-0 bg-border" />
1290
+
1291
+ {/* Footer */}
1292
+ <div className="flex shrink-0 items-center justify-between px-6 py-3">
1293
+ <div className="flex items-center gap-1.5">
1294
+ <Info className="size-3 shrink-0 text-muted-foreground" />
1295
+ <span className="text-xs text-muted-foreground">
1296
+ {t(
1297
+ 'customers.tags.manage.tenantNotice',
1298
+ 'Changes apply to the entire tenant \u00b7 visible immediately',
1299
+ )}
1300
+ </span>
1301
+ </div>
1302
+ <div className="flex items-center gap-2.5">
1303
+ <Button
1304
+ type="button"
1305
+ variant="outline"
1306
+ onClick={onClose}
1307
+ className="rounded-md border-border px-4 py-2.5 text-sm font-semibold text-foreground"
1308
+ >
1309
+ {t('customers.tags.manage.close', 'Cancel')}
1310
+ </Button>
1311
+ <Button
1312
+ type="button"
1313
+ onClick={() => {
1314
+ void handleSave()
1315
+ }}
1316
+ disabled={saving || !hasChanges}
1317
+ className="rounded-md bg-foreground px-4 py-2.5 text-sm font-semibold text-background hover:bg-foreground/90"
1318
+ >
1319
+ <Save className="mr-2 size-4" />
1320
+ {saving
1321
+ ? t('customers.tags.manage.saving', 'Saving...')
1322
+ : t('customers.tags.manage.save', 'Save changes')}
1323
+ </Button>
1324
+ </div>
1325
+ </div>
1326
+ </>
1327
+ )}
1328
+ </DialogContent>
1329
+ </Dialog>
1330
+ )
1331
+ }