@open-mercato/core 0.6.5-develop.5337.1.534b781eac → 0.6.5

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 (350) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/bootstrap.js +46 -6
  4. package/dist/bootstrap.js.map +2 -2
  5. package/dist/generated/entities/organization/index.js +2 -0
  6. package/dist/generated/entities/organization/index.js.map +2 -2
  7. package/dist/generated/entity-fields-registry.js +1 -0
  8. package/dist/generated/entity-fields-registry.js.map +2 -2
  9. package/dist/helpers/integration/crmFixtures.js +4 -0
  10. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  11. package/dist/modules/attachments/api/library/route.js +2 -2
  12. package/dist/modules/attachments/api/library/route.js.map +2 -2
  13. package/dist/modules/attachments/api/route.js +2 -0
  14. package/dist/modules/attachments/api/route.js.map +2 -2
  15. package/dist/modules/attachments/components/AttachmentContentPreview.js +9 -5
  16. package/dist/modules/attachments/components/AttachmentContentPreview.js.map +2 -2
  17. package/dist/modules/attachments/lib/access.js +18 -0
  18. package/dist/modules/attachments/lib/access.js.map +2 -2
  19. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js +3 -2
  20. package/dist/modules/audit_logs/api/audit-logs/actions/redo/route.js.map +2 -2
  21. package/dist/modules/audit_logs/data/entities.js +2 -1
  22. package/dist/modules/audit_logs/data/entities.js.map +2 -2
  23. package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
  24. package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
  25. package/dist/modules/audit_logs/services/accessLogService.js +10 -0
  26. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  27. package/dist/modules/auth/api/admin/nav.js +9 -0
  28. package/dist/modules/auth/api/admin/nav.js.map +2 -2
  29. package/dist/modules/auth/api/login.js +4 -13
  30. package/dist/modules/auth/api/login.js.map +2 -2
  31. package/dist/modules/auth/commands/users.js +20 -14
  32. package/dist/modules/auth/commands/users.js.map +2 -2
  33. package/dist/modules/auth/data/entities.js +4 -2
  34. package/dist/modules/auth/data/entities.js.map +2 -2
  35. package/dist/modules/auth/lib/backendChrome.js +35 -2
  36. package/dist/modules/auth/lib/backendChrome.js.map +2 -2
  37. package/dist/modules/auth/lib/consentIntegrity.js +3 -3
  38. package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
  39. package/dist/modules/auth/migrations/Migration20260610120000.js +30 -0
  40. package/dist/modules/auth/migrations/Migration20260610120000.js.map +7 -0
  41. package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
  42. package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
  43. package/dist/modules/auth/services/authService.js +5 -3
  44. package/dist/modules/auth/services/authService.js.map +2 -2
  45. package/dist/modules/auth/services/rbacService.js +3 -2
  46. package/dist/modules/auth/services/rbacService.js.map +2 -2
  47. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +1 -1
  48. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +1 -1
  49. package/dist/modules/catalog/ai-tools/products-pack.js.map +1 -1
  50. package/dist/modules/catalog/ai-tools/variants-pack.js.map +1 -1
  51. package/dist/modules/communication_channels/data/entities.js.map +1 -1
  52. package/dist/modules/communication_channels/encryption.js.map +1 -1
  53. package/dist/modules/communication_channels/lib/thread-matcher.js.map +1 -1
  54. package/dist/modules/communication_channels/lib/thread-token.js.map +1 -1
  55. package/dist/modules/currencies/api/currencies/route.js +4 -3
  56. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  57. package/dist/modules/customer_accounts/api/admin/roles.js +2 -1
  58. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  59. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
  60. package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
  61. package/dist/modules/customer_accounts/events.js +1 -1
  62. package/dist/modules/customer_accounts/events.js.map +1 -1
  63. package/dist/modules/customer_accounts/lib/resolveTenantContext.js.map +1 -1
  64. package/dist/modules/customers/acl.js +1 -1
  65. package/dist/modules/customers/acl.js.map +1 -1
  66. package/dist/modules/customers/ai-tools/companies-pack.js.map +1 -1
  67. package/dist/modules/customers/ai-tools/deals-pack.js.map +1 -1
  68. package/dist/modules/customers/ai-tools/people-pack.js.map +1 -1
  69. package/dist/modules/customers/api/companies/route.js +4 -4
  70. package/dist/modules/customers/api/companies/route.js.map +2 -2
  71. package/dist/modules/customers/api/deals/route.js +43 -2
  72. package/dist/modules/customers/api/deals/route.js.map +2 -2
  73. package/dist/modules/customers/api/deals/summary/route.js +402 -0
  74. package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
  75. package/dist/modules/customers/api/people/route.js +4 -4
  76. package/dist/modules/customers/api/people/route.js.map +2 -2
  77. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
  78. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
  79. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
  80. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
  81. package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
  82. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  83. package/dist/modules/customers/backend/customers/deals/page.js +221 -56
  84. package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
  85. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
  86. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  87. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
  88. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  89. package/dist/modules/customers/cli.js +15 -9
  90. package/dist/modules/customers/cli.js.map +2 -2
  91. package/dist/modules/customers/commands/addresses.js +5 -5
  92. package/dist/modules/customers/commands/addresses.js.map +2 -2
  93. package/dist/modules/customers/commands/comments.js +5 -5
  94. package/dist/modules/customers/commands/comments.js.map +2 -2
  95. package/dist/modules/customers/commands/deals.js +2 -2
  96. package/dist/modules/customers/commands/deals.js.map +2 -2
  97. package/dist/modules/customers/commands/entity-roles.js +2 -1
  98. package/dist/modules/customers/commands/entity-roles.js.map +2 -2
  99. package/dist/modules/customers/commands/interactions.js +8 -5
  100. package/dist/modules/customers/commands/interactions.js.map +2 -2
  101. package/dist/modules/customers/commands/shared.js +21 -6
  102. package/dist/modules/customers/commands/shared.js.map +2 -2
  103. package/dist/modules/customers/commands/tags.js +3 -3
  104. package/dist/modules/customers/commands/tags.js.map +2 -2
  105. package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
  106. package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
  107. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
  108. package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
  109. package/dist/modules/customers/components/detail/DealForm.js +100 -17
  110. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  111. package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
  112. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  113. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
  114. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  115. package/dist/modules/customers/components/detail/assignableStaff.js +21 -8
  116. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  117. package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
  118. package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
  119. package/dist/modules/customers/lib/dealsMetrics.js +82 -0
  120. package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
  121. package/dist/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.js.map +1 -1
  122. package/dist/modules/data_sync/api/run.js +1 -1
  123. package/dist/modules/data_sync/api/run.js.map +2 -2
  124. package/dist/modules/directory/api/organization-branding/route.js +214 -0
  125. package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
  126. package/dist/modules/directory/api/organizations/route.js +7 -0
  127. package/dist/modules/directory/api/organizations/route.js.map +3 -3
  128. package/dist/modules/directory/backend/directory/branding/page.js +214 -0
  129. package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
  130. package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
  131. package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
  132. package/dist/modules/directory/commands/organizations.js +8 -1
  133. package/dist/modules/directory/commands/organizations.js.map +2 -2
  134. package/dist/modules/directory/data/entities.js +3 -0
  135. package/dist/modules/directory/data/entities.js.map +2 -2
  136. package/dist/modules/directory/data/validators.js +9 -0
  137. package/dist/modules/directory/data/validators.js.map +2 -2
  138. package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
  139. package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
  140. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
  141. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
  142. package/dist/modules/directory/utils/organizationScope.js +59 -27
  143. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  144. package/dist/modules/entities/api/definitions.batch.js +2 -1
  145. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  146. package/dist/modules/entities/api/entities.js +7 -0
  147. package/dist/modules/entities/api/entities.js.map +2 -2
  148. package/dist/modules/entities/api/records.js +26 -15
  149. package/dist/modules/entities/api/records.js.map +2 -2
  150. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
  151. package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
  152. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
  153. package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
  154. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
  155. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  156. package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
  157. package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
  158. package/dist/modules/payment_gateways/api/transactions/route.js +2 -4
  159. package/dist/modules/payment_gateways/api/transactions/route.js.map +2 -2
  160. package/dist/modules/progress/api/jobs/[id]/route.js +7 -2
  161. package/dist/modules/progress/api/jobs/[id]/route.js.map +2 -2
  162. package/dist/modules/progress/api/jobs/route.js +1 -1
  163. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  164. package/dist/modules/progress/lib/progressServiceImpl.js +8 -2
  165. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  166. package/dist/modules/query_index/data/entities.js +2 -1
  167. package/dist/modules/query_index/data/entities.js.map +2 -2
  168. package/dist/modules/query_index/lib/engine.js +4 -2
  169. package/dist/modules/query_index/lib/engine.js.map +2 -2
  170. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
  171. package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
  172. package/dist/modules/resources/api/resources.js +2 -3
  173. package/dist/modules/resources/api/resources.js.map +2 -2
  174. package/dist/modules/sales/api/documents/factory.js +2 -2
  175. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  176. package/dist/modules/sales/commands/documents.js +7 -5
  177. package/dist/modules/sales/commands/documents.js.map +2 -2
  178. package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
  179. package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
  180. package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
  181. package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
  182. package/dist/modules/staff/api/team-members.js +9 -2
  183. package/dist/modules/staff/api/team-members.js.map +2 -2
  184. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
  185. package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
  186. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
  187. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  188. package/dist/modules/staff/commands/team-members.js +1 -1
  189. package/dist/modules/staff/commands/team-members.js.map +2 -2
  190. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  191. package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
  192. package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
  193. package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
  194. package/dist/modules/sync_excel/api/import/route.js +1 -1
  195. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  196. package/dist/modules/workflows/api/definitions/route.js +3 -2
  197. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  198. package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
  199. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  200. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
  201. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  202. package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
  203. package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
  204. package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
  205. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  206. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
  207. package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
  208. package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
  209. package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
  210. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
  211. package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
  212. package/generated/entities/organization/index.ts +1 -0
  213. package/generated/entity-fields-registry.ts +1 -0
  214. package/package.json +11 -12
  215. package/src/bootstrap.ts +65 -7
  216. package/src/helpers/integration/crmFixtures.ts +21 -1
  217. package/src/modules/attachments/AGENTS.md +79 -0
  218. package/src/modules/attachments/api/library/route.ts +2 -2
  219. package/src/modules/attachments/api/route.ts +2 -0
  220. package/src/modules/attachments/components/AttachmentContentPreview.tsx +6 -6
  221. package/src/modules/attachments/lib/access.ts +36 -0
  222. package/src/modules/audit_logs/api/audit-logs/actions/redo/route.ts +14 -2
  223. package/src/modules/audit_logs/data/entities.ts +1 -0
  224. package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
  225. package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
  226. package/src/modules/audit_logs/services/accessLogService.ts +15 -0
  227. package/src/modules/auth/api/admin/nav.ts +9 -0
  228. package/src/modules/auth/api/login.ts +13 -13
  229. package/src/modules/auth/commands/users.ts +32 -15
  230. package/src/modules/auth/data/entities.ts +13 -1
  231. package/src/modules/auth/i18n/de.json +0 -1
  232. package/src/modules/auth/i18n/en.json +0 -1
  233. package/src/modules/auth/i18n/es.json +0 -1
  234. package/src/modules/auth/i18n/pl.json +0 -1
  235. package/src/modules/auth/lib/backendChrome.tsx +37 -1
  236. package/src/modules/auth/lib/consentIntegrity.ts +6 -3
  237. package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -10
  238. package/src/modules/auth/migrations/Migration20260610120000.ts +53 -0
  239. package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
  240. package/src/modules/auth/services/authService.ts +24 -4
  241. package/src/modules/auth/services/rbacService.ts +11 -2
  242. package/src/modules/catalog/ai-tools/configuration-pack.ts +1 -1
  243. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +1 -1
  244. package/src/modules/catalog/ai-tools/products-pack.ts +1 -1
  245. package/src/modules/catalog/ai-tools/variants-pack.ts +1 -1
  246. package/src/modules/communication_channels/data/entities.ts +2 -2
  247. package/src/modules/communication_channels/encryption.ts +1 -1
  248. package/src/modules/communication_channels/lib/adapter.ts +1 -1
  249. package/src/modules/communication_channels/lib/thread-matcher.ts +1 -1
  250. package/src/modules/communication_channels/lib/thread-token.ts +1 -1
  251. package/src/modules/currencies/api/currencies/route.ts +4 -3
  252. package/src/modules/customer_accounts/api/admin/roles.ts +2 -1
  253. package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
  254. package/src/modules/customer_accounts/events.ts +1 -1
  255. package/src/modules/customer_accounts/lib/resolveTenantContext.ts +2 -2
  256. package/src/modules/customers/acl.ts +1 -1
  257. package/src/modules/customers/ai-tools/companies-pack.ts +1 -1
  258. package/src/modules/customers/ai-tools/deals-pack.ts +1 -1
  259. package/src/modules/customers/ai-tools/people-pack.ts +1 -1
  260. package/src/modules/customers/api/companies/route.ts +4 -4
  261. package/src/modules/customers/api/deals/route.ts +51 -2
  262. package/src/modules/customers/api/deals/summary/route.ts +496 -0
  263. package/src/modules/customers/api/people/route.ts +4 -4
  264. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
  265. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
  266. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
  267. package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
  268. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
  269. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
  270. package/src/modules/customers/cli.ts +15 -15
  271. package/src/modules/customers/commands/addresses.ts +5 -5
  272. package/src/modules/customers/commands/comments.ts +5 -5
  273. package/src/modules/customers/commands/deals.ts +2 -2
  274. package/src/modules/customers/commands/entity-roles.ts +2 -1
  275. package/src/modules/customers/commands/interactions.ts +8 -5
  276. package/src/modules/customers/commands/shared.ts +26 -4
  277. package/src/modules/customers/commands/tags.ts +3 -3
  278. package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
  279. package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
  280. package/src/modules/customers/components/detail/DealForm.tsx +121 -19
  281. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
  282. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
  283. package/src/modules/customers/components/detail/assignableStaff.ts +32 -8
  284. package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
  285. package/src/modules/customers/i18n/de.json +43 -0
  286. package/src/modules/customers/i18n/en.json +43 -0
  287. package/src/modules/customers/i18n/es.json +43 -0
  288. package/src/modules/customers/i18n/pl.json +43 -0
  289. package/src/modules/customers/lib/dealsMetrics.ts +159 -0
  290. package/src/modules/customers/migrations/Migration20260519120000_pipeline_stage_color_tones.ts +1 -1
  291. package/src/modules/data_sync/api/run.ts +1 -1
  292. package/src/modules/directory/api/organization-branding/route.ts +238 -0
  293. package/src/modules/directory/api/organizations/route.ts +7 -0
  294. package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
  295. package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
  296. package/src/modules/directory/commands/organizations.ts +9 -1
  297. package/src/modules/directory/data/entities.ts +3 -0
  298. package/src/modules/directory/data/validators.ts +12 -0
  299. package/src/modules/directory/i18n/de.json +21 -0
  300. package/src/modules/directory/i18n/en.json +21 -0
  301. package/src/modules/directory/i18n/es.json +21 -0
  302. package/src/modules/directory/i18n/pl.json +21 -0
  303. package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
  304. package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
  305. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
  306. package/src/modules/directory/utils/organizationScope.ts +85 -30
  307. package/src/modules/entities/api/definitions.batch.ts +11 -7
  308. package/src/modules/entities/api/entities.ts +11 -0
  309. package/src/modules/entities/api/records.ts +46 -25
  310. package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
  311. package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
  312. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
  313. package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
  314. package/src/modules/entities/i18n/de.json +1 -0
  315. package/src/modules/entities/i18n/en.json +1 -0
  316. package/src/modules/entities/i18n/es.json +1 -0
  317. package/src/modules/entities/i18n/pl.json +1 -0
  318. package/src/modules/payment_gateways/api/transactions/route.ts +2 -5
  319. package/src/modules/progress/api/jobs/[id]/route.ts +6 -1
  320. package/src/modules/progress/api/jobs/route.ts +1 -1
  321. package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
  322. package/src/modules/query_index/data/entities.ts +1 -0
  323. package/src/modules/query_index/lib/engine.ts +11 -5
  324. package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
  325. package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
  326. package/src/modules/resources/api/resources.ts +2 -3
  327. package/src/modules/sales/api/documents/factory.ts +2 -2
  328. package/src/modules/sales/commands/documents.ts +7 -5
  329. package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
  330. package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
  331. package/src/modules/staff/AGENTS.md +1 -1
  332. package/src/modules/staff/api/team-members.ts +9 -2
  333. package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
  334. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
  335. package/src/modules/staff/commands/team-members.ts +5 -2
  336. package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
  337. package/src/modules/staff/i18n/de.json +1 -0
  338. package/src/modules/staff/i18n/en.json +1 -0
  339. package/src/modules/staff/i18n/es.json +1 -0
  340. package/src/modules/staff/i18n/pl.json +1 -0
  341. package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
  342. package/src/modules/sync_excel/api/import/route.ts +1 -1
  343. package/src/modules/workflows/api/definitions/route.ts +3 -2
  344. package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
  345. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
  346. package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
  347. package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
  348. package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
  349. package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
  350. package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
@@ -6,6 +6,7 @@ import type { QueryEngine, QueryOptions, Where, Sort } from '@open-mercato/share
6
6
  import { normalizeExportFormat, serializeExport, defaultExportFilename, ensureColumns } from '@open-mercato/shared/lib/crud/exporters'
7
7
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
8
8
  import { resolveOrganizationScope, getSelectedOrganizationFromRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
9
+ import { SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, isOrmBackedSystemEntityId } from '@open-mercato/shared/lib/data/engine'
9
10
  import { parseBooleanToken, parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
10
11
  import { setRecordCustomFields } from '../lib/helpers'
11
12
  import { CustomFieldValue } from '../data/entities'
@@ -36,24 +37,34 @@ function isDeclaredCustomEntity(entityId: string): boolean {
36
37
 
37
38
  const CUSTOM_ENTITY_RECORD_RESOURCE_KIND = 'entities.record'
38
39
 
39
- async function detectCustomEntity(em: any, entityId: string): Promise<boolean> {
40
- if (isDeclaredCustomEntity(entityId)) return true
40
+ type RecordsEntityKind = 'system' | 'custom' | 'unknown'
41
+
42
+ // This surface manages doc-storage records, which exist for CUSTOM entities only.
43
+ // Module-declared ids backed by a registered ORM table are system entities — their
44
+ // records live in their own module tables/APIs, and stray doc rows for them poisoned
45
+ // read-path classification platform-wide (#2939) — so they are rejected outright. The
46
+ // previous fallback that classified an entity by the mere presence of
47
+ // `custom_entities_storage` rows is gone: within the allowed set, declaration (ce.ts)
48
+ // or an active `custom_entities` registration is authoritative.
49
+ async function classifyRecordsEntity(em: any, entityId: string): Promise<RecordsEntityKind> {
50
+ if (isOrmBackedSystemEntityId(em, entityId)) return 'system'
51
+ if (isDeclaredCustomEntity(entityId)) return 'custom'
41
52
  try {
42
53
  const { CustomEntity } = await import('../data/entities')
43
- const found = await em.findOne(CustomEntity as any, { entityId, isActive: true })
44
- if (found) return true
54
+ // Any registration row active or soft-deleted proves the id is a custom
55
+ // entity. Records persist beyond the definition's soft delete (TC-ENTITIES-006)
56
+ // and must stay readable/deletable, e.g. for the restore flow and cleanup.
57
+ const found = await em.findOne(CustomEntity as any, { entityId })
58
+ if (found) return 'custom'
45
59
  } catch {}
46
- try {
47
- const db = em.getKysely()
48
- const row = await db
49
- .selectFrom('custom_entities_storage' as any)
50
- .select(['entity_id' as any])
51
- .where('entity_type' as any, '=', entityId)
52
- .limit(1)
53
- .executeTakeFirst()
54
- return !!row
55
- } catch {}
56
- return false
60
+ return 'unknown'
61
+ }
62
+
63
+ function systemEntityRecordsRejection(entityId: string) {
64
+ return NextResponse.json(
65
+ { error: 'Records are available for custom entities only', code: SYSTEM_ENTITY_RECORDS_BLOCKED_CODE, entityId },
66
+ { status: 400 },
67
+ )
57
68
  }
58
69
 
59
70
  async function readCustomEntityRecordUpdatedAt(
@@ -148,13 +159,13 @@ export async function GET(req: Request) {
148
159
  const rbac = resolve('rbacService') as RbacService
149
160
  const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
150
161
  let organizationIds: string[] | null = scope.filterIds
151
- // Read/write symmetry: this endpoint writes every record to custom_entities_storage
152
- // via the data engine, including module-declared custom entities whose id is a
153
- // frozen system id and therefore never registered in `custom_entities`. detectCustomEntity
154
- // covers the declared-entity registry plus the custom_entities / doc-storage fallbacks
155
- // (mirrors HybridQueryEngine.isCustomEntity) so mapRow strips the cf_ prefix and the edit
156
- // form can read back saved values.
157
- const isCustomEntity = await detectCustomEntity(em, entityId)
162
+ // Module-declared custom entities (ce.ts) carry frozen system-style ids and are never
163
+ // registered in `custom_entities`, so classification checks the declared registry plus
164
+ // active registrations. System (table-backed) ids are rejected above; for the allowed
165
+ // set `isCustomEntity` drives mapRow's cf_ stripping so the edit form reads back values.
166
+ const entityKind = await classifyRecordsEntity(em, entityId)
167
+ if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
168
+ const isCustomEntity = entityKind === 'custom'
158
169
  await assertEntityAclForRequest({ auth, entityId, action: 'view', isCustomEntity, rbac })
159
170
  if (organizationIds && organizationIds.length === 0) {
160
171
  return NextResponse.json({ items: [], total: 0, page, pageSize, totalPages: 0 })
@@ -230,6 +241,10 @@ export async function GET(req: Request) {
230
241
  if (organizationIds && organizationIds.length) {
231
242
  qopts.organizationIds = organizationIds
232
243
  }
244
+ // Allowed entities are doc-storage-backed by definition (system ids were rejected
245
+ // above) — direct the engine to doc storage explicitly so reads stay deterministic
246
+ // even before the first record exists.
247
+ if (isCustomEntity) qopts.forceCustomEntityStorage = true
233
248
  for (const [k, v] of qpEntries) buildFilter(k, v, isCustomEntity)
234
249
  const res = await qe.query(entityId as any, qopts)
235
250
  const rawItems = res.items || []
@@ -363,7 +378,9 @@ export async function POST(req: Request) {
363
378
  const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
364
379
  const targetOrgId = scope.selectedId ?? auth.orgId
365
380
  if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
366
- const isCustomEntity = await detectCustomEntity(em, entityId)
381
+ const entityKind = await classifyRecordsEntity(em, entityId)
382
+ if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
383
+ const isCustomEntity = entityKind === 'custom'
367
384
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
368
385
  const norm = normalizeValues(values)
369
386
 
@@ -427,7 +444,9 @@ export async function PUT(req: Request) {
427
444
  const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
428
445
  const targetOrgId = scope.selectedId ?? auth.orgId
429
446
  if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
430
- const isCustomEntity = await detectCustomEntity(em, entityId)
447
+ const entityKind = await classifyRecordsEntity(em, entityId)
448
+ if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
449
+ const isCustomEntity = entityKind === 'custom'
431
450
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
432
451
  const norm = normalizeValues(values)
433
452
 
@@ -516,7 +535,9 @@ export async function DELETE(req: Request) {
516
535
  const scope = await resolveOrganizationScope({ em, rbac, auth, selectedId: getSelectedOrganizationFromRequest(req) })
517
536
  const targetOrgId = scope.selectedId ?? auth.orgId
518
537
  if (!targetOrgId) return NextResponse.json({ error: 'Organization context is required' }, { status: 400 })
519
- const isCustomEntity = await detectCustomEntity(em, entityId)
538
+ const entityKind = await classifyRecordsEntity(em, entityId)
539
+ if (entityKind === 'system') return systemEntityRecordsRejection(entityId)
540
+ const isCustomEntity = entityKind === 'custom'
520
541
  await assertEntityAclForRequest({ auth, entityId, action: 'manage', isCustomEntity, rbac })
521
542
  await de.deleteCustomEntityRecord({ entityId, recordId, organizationId: targetOrgId, tenantId: auth.tenantId!, soft: true })
522
543
  return NextResponse.json({ ok: true })
@@ -6,6 +6,8 @@ import { z } from 'zod'
6
6
  import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
7
7
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
8
8
  import { createCrudFormError, raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
9
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
10
+ import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
9
11
 
10
12
  type UpdateRecordRequest = (payload: { entityId: string; recordId: string; values: Record<string, unknown> }) => Promise<void>
11
13
 
@@ -38,6 +40,19 @@ export async function submitCustomEntityRecordUpdate(options: {
38
40
  type RecordsResponse = { items: any[] }
39
41
 
40
42
  export default function EditRecordPage({ params }: { params: { entityId?: string; recordId?: string } }) {
43
+ const t = useT()
44
+ const entityId = decodeURIComponent(params?.entityId || '')
45
+ const guard = useRecordsEntityGuard(entityId)
46
+ if (guard === 'blocked') {
47
+ return <ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
48
+ }
49
+ if (guard === 'checking') {
50
+ return <LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
51
+ }
52
+ return <EditRecordPageInner params={params} />
53
+ }
54
+
55
+ function EditRecordPageInner({ params }: { params: { entityId?: string; recordId?: string } }) {
41
56
  const t = useT()
42
57
  const entityId = decodeURIComponent(params?.entityId || '')
43
58
  const recordId = decodeURIComponent(params?.recordId || '')
@@ -6,6 +6,8 @@ import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
6
6
  import { z } from 'zod'
7
7
  import { createCrud } from '@open-mercato/ui/backend/utils/crud'
8
8
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
10
+ import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
9
11
 
10
12
  type CreateRecordRequest = (payload: { entityId: string; values: Record<string, unknown> }) => Promise<void>
11
13
 
@@ -30,6 +32,19 @@ export async function submitCustomEntityRecord(options: {
30
32
  }
31
33
 
32
34
  export default function CreateRecordPage({ params }: { params: { entityId?: string } }) {
35
+ const t = useT()
36
+ const entityId = decodeURIComponent(params?.entityId || '')
37
+ const guard = useRecordsEntityGuard(entityId)
38
+ if (guard === 'blocked') {
39
+ return <ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
40
+ }
41
+ if (guard === 'checking') {
42
+ return <LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
43
+ }
44
+ return <CreateRecordPageInner params={params} />
45
+ }
46
+
47
+ function CreateRecordPageInner({ params }: { params: { entityId?: string } }) {
33
48
  const t = useT()
34
49
  const router = useRouter()
35
50
  const entityId = decodeURIComponent(params?.entityId || '')
@@ -17,6 +17,9 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
17
17
  import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
18
18
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
19
19
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
20
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
21
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
22
+ import { useRecordsEntityGuard } from '@open-mercato/core/modules/entities/components/useRecordsEntityGuard'
20
23
 
21
24
  type RecordsResponse = {
22
25
  items: any[]
@@ -44,6 +47,26 @@ function normalizeCell(v: any): string {
44
47
  }
45
48
 
46
49
  export default function RecordsPage({ params }: { params: { entityId?: string } }) {
50
+ const t = useT()
51
+ const entityId = decodeURIComponent(params?.entityId || '')
52
+ const guard = useRecordsEntityGuard(entityId)
53
+ if (guard !== 'allowed') {
54
+ return (
55
+ <Page>
56
+ <PageBody>
57
+ {guard === 'blocked' ? (
58
+ <ErrorMessage label={t('entities.userEntities.records.errors.systemEntity', 'This entity is system-managed. Records are available for custom entities only.')} />
59
+ ) : (
60
+ <LoadingMessage label={t('entities.userEntities.records.loading', 'Loading records...')} />
61
+ )}
62
+ </PageBody>
63
+ </Page>
64
+ )
65
+ }
66
+ return <RecordsPageInner params={params} />
67
+ }
68
+
69
+ function RecordsPageInner({ params }: { params: { entityId?: string } }) {
47
70
  const entityId = decodeURIComponent(params?.entityId || '')
48
71
  const [sorting, setSorting] = React.useState<SortingState>([{ id: 'id', desc: false }])
49
72
  const [page, setPage] = React.useState(1)
@@ -0,0 +1,41 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
4
+
5
+ export type RecordsEntityGuardState = 'checking' | 'blocked' | 'allowed'
6
+
7
+ // Mirrors SYSTEM_ENTITY_RECORDS_BLOCKED_CODE from @open-mercato/shared/lib/data/engine —
8
+ // kept as a literal so client bundles do not pull the server-side data engine in.
9
+ const SYSTEM_ENTITY_RECORDS_BLOCKED_CODE = 'system_entity_records_blocked'
10
+
11
+ /**
12
+ * The records surface serves custom entities only; the API rejects system
13
+ * (table-backed) entity ids with 400 + `system_entity_records_blocked` (#2939
14
+ * hardening). Records pages are URL-addressable for any entity id, so they probe
15
+ * once and render a dedicated error state instead of a broken table/form.
16
+ * Fails open on transport errors — the page's own data calls surface those.
17
+ */
18
+ export function useRecordsEntityGuard(entityId: string): RecordsEntityGuardState {
19
+ const [state, setState] = React.useState<RecordsEntityGuardState>(entityId ? 'checking' : 'allowed')
20
+ React.useEffect(() => {
21
+ if (!entityId) {
22
+ setState('allowed')
23
+ return
24
+ }
25
+ let cancelled = false
26
+ setState('checking')
27
+ apiCall<{ code?: string }>(`/api/entities/records?entityId=${encodeURIComponent(entityId)}&page=1&pageSize=1`)
28
+ .then((res) => {
29
+ if (cancelled) return
30
+ const blocked = res.status === 400 && res.result?.code === SYSTEM_ENTITY_RECORDS_BLOCKED_CODE
31
+ setState(blocked ? 'blocked' : 'allowed')
32
+ })
33
+ .catch(() => {
34
+ if (!cancelled) setState('allowed')
35
+ })
36
+ return () => {
37
+ cancelled = true
38
+ }
39
+ }, [entityId])
40
+ return state
41
+ }
@@ -82,6 +82,7 @@
82
82
  "entities.userEntities.records.errors.deleteFailed": "Datensatz konnte nicht gelöscht werden",
83
83
  "entities.userEntities.records.errors.entityIdRequired": "Entitätskennung ist erforderlich",
84
84
  "entities.userEntities.records.errors.recordIdRequired": "Datensatzkennung ist erforderlich",
85
+ "entities.userEntities.records.errors.systemEntity": "Diese Entität wird vom System verwaltet. Datensätze sind nur für benutzerdefinierte Entitäten verfügbar.",
85
86
  "entities.userEntities.records.form.createTitle": "Datensatz erstellen",
86
87
  "entities.userEntities.records.form.editTitle": "Datensatz bearbeiten",
87
88
  "entities.userEntities.records.form.submitCreate": "Erstellen",
@@ -82,6 +82,7 @@
82
82
  "entities.userEntities.records.errors.deleteFailed": "Failed to delete record",
83
83
  "entities.userEntities.records.errors.entityIdRequired": "Entity identifier is required",
84
84
  "entities.userEntities.records.errors.recordIdRequired": "Record identifier is required",
85
+ "entities.userEntities.records.errors.systemEntity": "This entity is system-managed. Records are available for custom entities only.",
85
86
  "entities.userEntities.records.form.createTitle": "Create record",
86
87
  "entities.userEntities.records.form.editTitle": "Edit record",
87
88
  "entities.userEntities.records.form.submitCreate": "Create",
@@ -82,6 +82,7 @@
82
82
  "entities.userEntities.records.errors.deleteFailed": "No se pudo eliminar el registro",
83
83
  "entities.userEntities.records.errors.entityIdRequired": "Se requiere el identificador de la entidad",
84
84
  "entities.userEntities.records.errors.recordIdRequired": "Se requiere el identificador del registro",
85
+ "entities.userEntities.records.errors.systemEntity": "Esta entidad está gestionada por el sistema. Los registros solo están disponibles para entidades personalizadas.",
85
86
  "entities.userEntities.records.form.createTitle": "Crear registro",
86
87
  "entities.userEntities.records.form.editTitle": "Editar registro",
87
88
  "entities.userEntities.records.form.submitCreate": "Crear",
@@ -82,6 +82,7 @@
82
82
  "entities.userEntities.records.errors.deleteFailed": "Nie udało się usunąć rekordu",
83
83
  "entities.userEntities.records.errors.entityIdRequired": "Wymagany jest identyfikator encji",
84
84
  "entities.userEntities.records.errors.recordIdRequired": "Wymagany jest identyfikator rekordu",
85
+ "entities.userEntities.records.errors.systemEntity": "Ta encja jest zarządzana systemowo. Rekordy są dostępne wyłącznie dla encji niestandardowych.",
85
86
  "entities.userEntities.records.form.createTitle": "Utwórz rekord",
86
87
  "entities.userEntities.records.form.editTitle": "Edytuj rekord",
87
88
  "entities.userEntities.records.form.submitCreate": "Utwórz",
@@ -5,16 +5,13 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
5
  import { GatewayTransaction } from '../../data/entities'
6
6
  import { listTransactionsQuerySchema } from '../../data/validators'
7
7
  import { paymentGatewaysTag } from '../openapi'
8
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
8
9
 
9
10
  export const metadata = {
10
11
  path: '/payment_gateways/transactions',
11
12
  GET: { requireAuth: true, requireFeatures: ['payment_gateways.view'] },
12
13
  }
13
14
 
14
- function escapeLikePattern(value: string): string {
15
- return value.replace(/[\\%_]/g, '\\$&')
16
- }
17
-
18
15
  function formatDateValue(value: unknown): string | null {
19
16
  if (!value) return null
20
17
  if (value instanceof Date) return value.toISOString()
@@ -58,7 +55,7 @@ export async function GET(req: Request) {
58
55
  qb.andWhere({ unifiedStatus: status })
59
56
  }
60
57
  if (search) {
61
- const pattern = `%${escapeLikePattern(search)}%`
58
+ const pattern = buildIlikeTerm(search)
62
59
  qb.andWhere(`(
63
60
  cast(gt.id as text) ilike ?
64
61
  or cast(gt.payment_id as text) ilike ?
@@ -26,6 +26,7 @@ export async function GET(req: Request, { params }: { params: { id: string } })
26
26
  const job = await em.findOne(ProgressJob, {
27
27
  id: params.id,
28
28
  tenantId: auth.tenantId,
29
+ ...(auth.orgId ? { organizationId: auth.orgId } : {}),
29
30
  })
30
31
 
31
32
  if (!job) {
@@ -74,7 +75,11 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
74
75
 
75
76
  const container = await createRequestContainer()
76
77
  const em = container.resolve('em') as EntityManager
77
- const existing = await em.findOne(ProgressJob, { id: params.id, tenantId: auth.tenantId })
78
+ const existing = await em.findOne(ProgressJob, {
79
+ id: params.id,
80
+ tenantId: auth.tenantId,
81
+ ...(auth.orgId ? { organizationId: auth.orgId } : {}),
82
+ })
78
83
  if (!existing) {
79
84
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
80
85
  }
@@ -165,7 +165,7 @@ export async function POST(req: Request) {
165
165
  const stack = error instanceof Error ? error.stack : undefined
166
166
  console.error('[progress.jobs.create] unhandled error', { message, stack })
167
167
  return NextResponse.json(
168
- { error: 'Failed to create progress job.', message, stack },
168
+ { error: 'Failed to create progress job.' },
169
169
  { status: 500 },
170
170
  )
171
171
  }
@@ -75,7 +75,11 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
75
75
  },
76
76
 
77
77
  async updateProgress(jobId, input, ctx) {
78
- const job = await em.findOneOrFail(ProgressJob, { id: jobId, tenantId: ctx.tenantId })
78
+ const job = await em.findOneOrFail(ProgressJob, {
79
+ id: jobId,
80
+ tenantId: ctx.tenantId,
81
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
82
+ })
79
83
  if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
80
84
  return job
81
85
  }
@@ -197,6 +201,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
197
201
  const job = await em.findOneOrFail(ProgressJob, {
198
202
  id: jobId,
199
203
  tenantId: ctx.tenantId,
204
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
200
205
  cancellable: true,
201
206
  status: { $in: ['pending', 'running'] },
202
207
  })
@@ -279,6 +284,7 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
279
284
  return em.findOne(ProgressJob, {
280
285
  id: jobId,
281
286
  tenantId: ctx.tenantId,
287
+ ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),
282
288
  })
283
289
  },
284
290
 
@@ -254,6 +254,7 @@ export class IndexerStatusLog {
254
254
  @Entity({ tableName: 'search_tokens' })
255
255
  @Index({ name: 'search_tokens_lookup_idx', properties: ['entityType', 'field', 'tokenHash', 'tenantId', 'organizationId'] })
256
256
  @Index({ name: 'search_tokens_entity_idx', properties: ['entityType', 'entityId'] })
257
+ @Index({ name: 'search_tokens_tenant_token_hash_idx', properties: ['tenantId', 'tokenHash'] })
257
258
  export class SearchToken {
258
259
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
259
260
  id!: string
@@ -2,7 +2,7 @@ import type { QueryEngine, QueryOptions, QueryResult, FilterOp, Filter, QueryCus
2
2
  import { SortDir } from '@open-mercato/shared/lib/query/types'
3
3
  import type { EntityId } from '@open-mercato/shared/modules/entities'
4
4
  import type { EntityManager } from '@mikro-orm/postgresql'
5
- import { BasicQueryEngine, resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
5
+ import { BasicQueryEngine, resolveEntityTableName, resolveRegisteredEntityTableName } from '@open-mercato/shared/lib/query/engine'
6
6
  import { type Kysely, sql, type RawBuilder } from 'kysely'
7
7
  import type { EventBus } from '@open-mercato/events'
8
8
  import { readCoverageSnapshot, refreshCoverageSnapshot } from './coverage'
@@ -219,7 +219,7 @@ export class HybridQueryEngine implements QueryEngine {
219
219
  const debugEnabled = this.isDebugVerbosity()
220
220
  if (debugEnabled) this.debug('query:start', { entity })
221
221
 
222
- const isCustom = await this.isCustomEntity(entity)
222
+ const isCustom = opts.forceCustomEntityStorage === true || await this.isCustomEntity(entity)
223
223
  if (isCustom) {
224
224
  if (debugEnabled) this.debug('query:custom-entity', { entity })
225
225
  const section = profiler.section('custom_entity')
@@ -1005,6 +1005,14 @@ export class HybridQueryEngine implements QueryEngine {
1005
1005
  .executeTakeFirst()
1006
1006
  if (row) {
1007
1007
  result = true
1008
+ } else if (resolveRegisteredEntityTableName(this.em, entity) !== null) {
1009
+ // An id backed by a registered ORM table is never doc-storage-backed by
1010
+ // inference: stray `custom_entities_storage` rows for such an id (e.g. written
1011
+ // through the generic entities data engine) must not hijack every list/detail
1012
+ // read for the whole entity type away from its base table (#2939). Surfaces
1013
+ // that intentionally read doc records for a dual-declared id pass
1014
+ // `forceCustomEntityStorage` in QueryOptions instead.
1015
+ result = false
1008
1016
  } else {
1009
1017
  // Read/write symmetry. Records written through the entities data engine
1010
1018
  // (`de.createCustomEntityRecord`) always land in `custom_entities_storage`,
@@ -1012,9 +1020,7 @@ export class HybridQueryEngine implements QueryEngine {
1012
1020
  // id — those are NEVER registered in `custom_entities` (install treats a
1013
1021
  // system id as non-registrable). Without this fallback the query routes to
1014
1022
  // the empty ORM/index path and those records are write-only (created with
1015
- // 200 but unreadable on the edit form). A real ORM entity never writes rows
1016
- // to `custom_entities_storage`, so this can only ever re-classify genuine
1017
- // doc-storage entities — it cannot misroute table-backed entities.
1023
+ // 200 but unreadable on the edit form).
1018
1024
  result = await this.hasCustomEntityStorageRows(entity)
1019
1025
  }
1020
1026
  } catch {
@@ -1354,6 +1354,17 @@
1354
1354
  "primary": false,
1355
1355
  "unique": false
1356
1356
  },
1357
+ {
1358
+ "columnNames": [
1359
+ "tenant_id",
1360
+ "token_hash"
1361
+ ],
1362
+ "composite": true,
1363
+ "constraint": false,
1364
+ "keyName": "search_tokens_tenant_token_hash_idx",
1365
+ "primary": false,
1366
+ "unique": false
1367
+ },
1357
1368
  {
1358
1369
  "columnNames": [
1359
1370
  "id"
@@ -0,0 +1,29 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ // #2966: TokenSearchStrategy — the always-available global-search fallback —
4
+ // filters search_tokens by token_hash IN (...) AND tenant_id on every
5
+ // keystroke, but both existing indexes lead with entity_type (and the lookup
6
+ // index interposes field, which the query never filters), so the per-keystroke
7
+ // lookup degrades to a sequential scan as the table grows. Add a
8
+ // (tenant_id, token_hash)-leading index so it becomes an index scan.
9
+ //
10
+ // search_tokens is high-churn (rows scale with records × tokens), so the
11
+ // index is built CONCURRENTLY to avoid blocking writes during the build.
12
+ // CREATE INDEX CONCURRENTLY cannot run inside a transaction, hence
13
+ // isTransactional() => false; the migration runner applies migrations
14
+ // one-by-one, so this opt-out is safe.
15
+ export class Migration20260611103000_query_index extends Migration {
16
+
17
+ override isTransactional(): boolean {
18
+ return false;
19
+ }
20
+
21
+ override up(): void | Promise<void> {
22
+ this.addSql(`create index concurrently if not exists "search_tokens_tenant_token_hash_idx" on "search_tokens" ("tenant_id", "token_hash");`);
23
+ }
24
+
25
+ override down(): void | Promise<void> {
26
+ this.addSql(`drop index if exists "search_tokens_tenant_token_hash_idx";`);
27
+ }
28
+
29
+ }
@@ -2,7 +2,7 @@ import { z } from 'zod'
2
2
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
3
3
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
4
4
  import { resolveCrudRecordId, parseScopedCommandInput } from '@open-mercato/shared/lib/api/scoped'
5
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
5
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
6
6
  import type { EntityManager } from '@mikro-orm/postgresql'
7
7
  import { ResourcesResource, ResourcesResourceTagAssignment, ResourcesResourceTag } from '../data/entities'
8
8
  import { resourcesResourceCreateSchema, resourcesResourceUpdateSchema } from '../data/validators'
@@ -107,8 +107,7 @@ const crud = makeCrudRoute({
107
107
  }
108
108
  const term = sanitizeSearchTerm(query.search)
109
109
  if (term) {
110
- const like = `%${escapeLikePattern(term)}%`
111
- filters[F.name] = { $ilike: like }
110
+ filters[F.name] = { $ilike: buildIlikeTerm(term) }
112
111
  }
113
112
  if (query.resourceTypeId) {
114
113
  filters[F.resource_type_id] = query.resourceTypeId
@@ -17,7 +17,7 @@ import {
17
17
  } from '../openapi'
18
18
  import { parseScopedCommandInput, resolveCrudRecordId } from '../utils'
19
19
  import { documentUpdateSchema } from '../../commands/documents'
20
- import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
20
+ import { buildIlikeTerm } from '@open-mercato/shared/lib/db/buildIlikeTerm'
21
21
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
22
22
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
23
23
  import { recalculateOrderTotalsForDisplay } from '../../commands/returns'
@@ -101,7 +101,7 @@ function buildFilters(query: ListQuery, numberColumn: string, kind: DocumentKind
101
101
  const filters: Record<string, unknown> = {}
102
102
  if (query.id) filters.id = { $eq: query.id }
103
103
  if (query.search && query.search.trim().length > 0) {
104
- const term = `%${escapeLikePattern(query.search.trim())}%`
104
+ const term = buildIlikeTerm(query.search.trim())
105
105
  filters[numberColumn] = { $ilike: term }
106
106
  }
107
107
  if (query.customerId) {
@@ -719,11 +719,13 @@ async function resolveAddressSnapshot(
719
719
  addressId?: string | null,
720
720
  ): Promise<Record<string, unknown> | null> {
721
721
  if (!addressId) return null;
722
- const address = await em.findOne(CustomerAddress, {
723
- id: addressId,
724
- organizationId,
725
- tenantId,
726
- });
722
+ const address = await findOneWithDecryption(
723
+ em,
724
+ CustomerAddress,
725
+ { id: addressId, organizationId, tenantId },
726
+ undefined,
727
+ { tenantId, organizationId },
728
+ );
727
729
  if (!address) return null;
728
730
 
729
731
  return {
@@ -23,6 +23,7 @@ import {
23
23
  createDictionaryMap,
24
24
  normalizeDictionaryEntries,
25
25
  } from '@open-mercato/core/modules/dictionaries/components/dictionaryAppearance'
26
+ import { SALES_DOCUMENT_NUMBER_COLUMN_META } from './salesDocumentsColumns'
26
27
 
27
28
  type SalesDocumentKind = 'order' | 'quote'
28
29
 
@@ -594,7 +595,7 @@ export function SalesDocumentsTable({ kind }: { kind: SalesDocumentKind }) {
594
595
  ) : null}
595
596
  </div>
596
597
  ),
597
- meta: { sticky: true },
598
+ meta: SALES_DOCUMENT_NUMBER_COLUMN_META,
598
599
  },
599
600
  {
600
601
  accessorKey: 'customerName',
@@ -0,0 +1,6 @@
1
+ export const SALES_DOCUMENT_NUMBER_COLUMN_MAX_WIDTH = '220px'
2
+
3
+ export const SALES_DOCUMENT_NUMBER_COLUMN_META = {
4
+ sticky: true,
5
+ maxWidth: SALES_DOCUMENT_NUMBER_COLUMN_MAX_WIDTH,
6
+ } as const
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `staff` module is **optional** and slated for extraction into a standalone `@open-mercato/staff` package published from the [official-modules](https://github.com/open-mercato/official-modules) repository. Core modules MUST NOT take direct dependencies on staff entities, helpers, or services — cross-module contact happens through the public surfaces listed below.
4
4
 
5
- See [`.ai/specs/2026-05-08-staff-decouple-from-core.md`](../../../../../.ai/specs/2026-05-08-staff-decouple-from-core.md) for the decoupling plan, and [`BACKWARD_COMPATIBILITY.md`](../../../../../BACKWARD_COMPATIBILITY.md) for the contract-surface taxonomy referenced below.
5
+ See [`.ai/specs/implemented/2026-05-08-staff-decouple-from-core.md`](../../../../../.ai/specs/implemented/2026-05-08-staff-decouple-from-core.md) for the decoupling plan, and [`BACKWARD_COMPATIBILITY.md`](../../../../../BACKWARD_COMPATIBILITY.md) for the contract-surface taxonomy referenced below.
6
6
 
7
7
  ## MUST Rules
8
8
 
@@ -225,7 +225,12 @@ const crud = makeCrudRoute({
225
225
  const { translate } = await resolveTranslations()
226
226
  return parseScopedCommandInput(staffTeamMemberUpdateSchema, raw ?? {}, ctx, translate)
227
227
  },
228
- response: () => ({ ok: true }),
228
+ // Surface the freshly-bumped updatedAt so inline (non-CrudForm) callers can
229
+ // refresh their optimistic-lock token between sequential edits (#2848).
230
+ response: (arg: { result?: { updatedAt?: string | null } | null }) => ({
231
+ ok: true,
232
+ updatedAt: arg?.result?.updatedAt ?? null,
233
+ }),
229
234
  },
230
235
  delete: {
231
236
  commandId: 'staff.team-members.delete',
@@ -287,7 +292,9 @@ export const openApi = createStaffCrudOpenApi({
287
292
  },
288
293
  update: {
289
294
  schema: staffTeamMemberUpdateSchema,
290
- responseSchema: defaultOkResponseSchema,
295
+ responseSchema: defaultOkResponseSchema.extend({
296
+ updatedAt: z.string().nullable().optional(),
297
+ }),
291
298
  description: 'Updates a team member by id.',
292
299
  },
293
300
  del: {