@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6331.1.a33b8e99b7

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 (244) hide show
  1. package/dist/modules/attachments/components/AttachmentPartitionSettings.js +3 -0
  2. package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
  3. package/dist/modules/auth/api/roles/acl/route.js +2 -2
  4. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  5. package/dist/modules/auth/api/users/acl/route.js +2 -2
  6. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  7. package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
  8. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  9. package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
  10. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  11. package/dist/modules/auth/data/entities.js +2 -2
  12. package/dist/modules/auth/data/entities.js.map +2 -2
  13. package/dist/modules/business_rules/api/rules/route.js +3 -3
  14. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  15. package/dist/modules/business_rules/api/sets/route.js +3 -3
  16. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  17. package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
  18. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  19. package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
  20. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  21. package/dist/modules/catalog/api/product-media/route.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
  23. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  25. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  26. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
  27. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  28. package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
  29. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  30. package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
  31. package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
  32. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  33. package/dist/modules/currencies/backend/currencies/page.js +2 -1
  34. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  35. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
  36. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  37. package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
  38. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  39. package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
  40. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  41. package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
  42. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  43. package/dist/modules/customers/api/addresses/route.js +4 -2
  44. package/dist/modules/customers/api/addresses/route.js.map +2 -2
  45. package/dist/modules/customers/api/tags/route.js +8 -3
  46. package/dist/modules/customers/api/tags/route.js.map +2 -2
  47. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
  48. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  49. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
  50. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  51. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
  52. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  53. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
  54. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
  55. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
  56. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  57. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
  58. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  59. package/dist/modules/customers/commands/interactions.js +5 -5
  60. package/dist/modules/customers/commands/interactions.js.map +2 -2
  61. package/dist/modules/customers/commands/pipeline-stages.js +24 -2
  62. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  63. package/dist/modules/customers/commands/pipelines.js +24 -2
  64. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  65. package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
  66. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  67. package/dist/modules/customers/components/detail/DealForm.js +5 -1
  68. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  69. package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
  70. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  71. package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
  72. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  73. package/dist/modules/data_sync/api/schedules/route.js +1 -1
  74. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  75. package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
  76. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  77. package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
  78. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  79. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
  80. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  81. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
  82. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  83. package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
  84. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  85. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  86. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
  87. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  88. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
  89. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  90. package/dist/modules/entities/api/entities.js +2 -2
  91. package/dist/modules/entities/api/entities.js.map +2 -2
  92. package/dist/modules/entities/api/records.js +7 -5
  93. package/dist/modules/entities/api/records.js.map +2 -2
  94. package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
  95. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  96. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  97. package/dist/modules/feature_toggles/data/entities.js +1 -1
  98. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  99. package/dist/modules/feature_toggles/data/validators.js +2 -1
  100. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  101. package/dist/modules/feature_toggles/lib/queries.js +2 -1
  102. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  103. package/dist/modules/inbox_ops/api/settings/route.js +2 -2
  104. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  105. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
  106. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  107. package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
  108. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  109. package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
  110. package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
  111. package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
  112. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  113. package/dist/modules/sales/commands/documents.js +11 -10
  114. package/dist/modules/sales/commands/documents.js.map +2 -2
  115. package/dist/modules/sales/commands/payments.js +6 -1
  116. package/dist/modules/sales/commands/payments.js.map +2 -2
  117. package/dist/modules/sales/commands/returns.js +3 -3
  118. package/dist/modules/sales/commands/returns.js.map +2 -2
  119. package/dist/modules/sales/commands/shared.js +3 -3
  120. package/dist/modules/sales/commands/shared.js.map +2 -2
  121. package/dist/modules/sales/commands/shipments.js +6 -1
  122. package/dist/modules/sales/commands/shipments.js.map +2 -2
  123. package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
  124. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  125. package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
  126. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  127. package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
  128. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  129. package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
  130. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  131. package/dist/modules/sales/di.js +39 -3
  132. package/dist/modules/sales/di.js.map +2 -2
  133. package/dist/modules/sales/setup.js +2 -0
  134. package/dist/modules/sales/setup.js.map +2 -2
  135. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
  136. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  137. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
  138. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  139. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
  140. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  141. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
  142. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  143. package/dist/modules/staff/commands/job-histories.js +3 -3
  144. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  145. package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
  146. package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
  147. package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
  148. package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
  149. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
  150. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  151. package/dist/modules/translations/components/TranslationManager.js +7 -1
  152. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  153. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
  154. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  155. package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
  156. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  157. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
  158. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  159. package/package.json +7 -7
  160. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
  161. package/src/modules/auth/api/roles/acl/route.ts +2 -2
  162. package/src/modules/auth/api/users/acl/route.ts +2 -2
  163. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
  164. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
  165. package/src/modules/auth/data/entities.ts +2 -2
  166. package/src/modules/business_rules/api/rules/route.ts +3 -3
  167. package/src/modules/business_rules/api/sets/route.ts +3 -3
  168. package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
  169. package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
  170. package/src/modules/catalog/api/product-media/route.ts +4 -0
  171. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
  172. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
  173. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
  174. package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
  175. package/src/modules/catalog/lib/bulkDelete.ts +6 -0
  176. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
  177. package/src/modules/currencies/backend/currencies/page.tsx +2 -1
  178. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
  179. package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
  180. package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
  181. package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
  182. package/src/modules/customers/api/addresses/route.ts +2 -0
  183. package/src/modules/customers/api/tags/route.ts +6 -1
  184. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
  185. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
  186. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
  187. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
  188. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  189. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
  190. package/src/modules/customers/commands/interactions.ts +5 -5
  191. package/src/modules/customers/commands/pipeline-stages.ts +26 -2
  192. package/src/modules/customers/commands/pipelines.ts +26 -2
  193. package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
  194. package/src/modules/customers/components/detail/DealForm.tsx +12 -0
  195. package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
  196. package/src/modules/customers/i18n/de.json +2 -0
  197. package/src/modules/customers/i18n/en.json +2 -0
  198. package/src/modules/customers/i18n/es.json +2 -0
  199. package/src/modules/customers/i18n/pl.json +2 -0
  200. package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
  201. package/src/modules/data_sync/api/schedules/route.ts +1 -1
  202. package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
  203. package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
  204. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
  205. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
  206. package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
  207. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
  208. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
  209. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
  210. package/src/modules/entities/api/entities.ts +2 -2
  211. package/src/modules/entities/api/records.ts +7 -5
  212. package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
  213. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
  214. package/src/modules/feature_toggles/data/entities.ts +1 -1
  215. package/src/modules/feature_toggles/data/validators.ts +1 -0
  216. package/src/modules/feature_toggles/lib/queries.ts +1 -0
  217. package/src/modules/inbox_ops/api/settings/route.ts +2 -2
  218. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
  219. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
  220. package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
  221. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
  222. package/src/modules/sales/commands/documents.ts +11 -10
  223. package/src/modules/sales/commands/payments.ts +11 -0
  224. package/src/modules/sales/commands/returns.ts +3 -3
  225. package/src/modules/sales/commands/shared.ts +10 -4
  226. package/src/modules/sales/commands/shipments.ts +14 -0
  227. package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
  228. package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
  229. package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
  230. package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
  231. package/src/modules/sales/di.ts +68 -9
  232. package/src/modules/sales/setup.ts +2 -0
  233. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
  234. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
  235. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
  236. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
  237. package/src/modules/staff/commands/job-histories.ts +3 -3
  238. package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
  239. package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
  240. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
  241. package/src/modules/translations/components/TranslationManager.tsx +14 -1
  242. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
  243. package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
  244. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
@@ -30,6 +30,8 @@ import {
30
30
  ensureTenantScope,
31
31
  extractUndoPayload,
32
32
  toNumericString,
33
+ enforceSalesDocumentOptimisticLock,
34
+ SALES_RESOURCE_KIND_ORDER,
33
35
  } from './shared'
34
36
  import { resolveDictionaryEntryValue } from '../lib/dictionaries'
35
37
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
@@ -343,6 +345,9 @@ const createPaymentCommand: CommandHandler<
343
345
  if (order.deletedAt) {
344
346
  throw new CrudHttpError(404, { error: 'sales.payments.order_not_found' })
345
347
  }
348
+ // Guard the parent order's aggregate version (Gap A): a payment mutation
349
+ // recalculates the order totals, so a stale parent must 409 before we touch it.
350
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
346
351
  if (
347
352
  order.currencyCode &&
348
353
  input.currencyCode &&
@@ -730,6 +735,9 @@ const updatePaymentCommand: CommandHandler<
730
735
  )
731
736
  ensureSameScope(payment, resolvedOrganizationId, resolvedTenantId)
732
737
  const previousOrder = payment.order as SalesOrder | null
738
+ // Guard the parent order's aggregate version (Gap A): updating a payment
739
+ // recalculates the order totals, so a stale parent must 409 before mutating.
740
+ await enforceSalesDocumentOptimisticLock(ctx, previousOrder, SALES_RESOURCE_KIND_ORDER)
733
741
  // Apply payment scalar fields, order/line status changes and the
734
742
  // allocations rebuild in one transaction so a mid-write failure cannot
735
743
  // leave the payment and its allocations partially committed (#2336).
@@ -1064,6 +1072,9 @@ const deletePaymentCommand: CommandHandler<
1064
1072
  )
1065
1073
  ensureSameScope(payment, input.organizationId, input.tenantId)
1066
1074
  const order = payment.order as SalesOrder | null
1075
+ // Guard the parent order's aggregate version (Gap A): deleting a payment
1076
+ // recalculates the order totals, so a stale parent must 409 before mutating.
1077
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
1067
1078
  const allocations = await findWithDecryption(em, SalesPaymentAllocation, { payment }, {}, { tenantId: payment.tenantId, organizationId: payment.organizationId })
1068
1079
  const allocationOrders = allocations
1069
1080
  .map((allocation) =>
@@ -615,7 +615,7 @@ const createReturnCommand: CommandHandler<ReturnCreateInput, { returnId: string
615
615
  throw new CrudHttpError(404, { error: translate('sales.returns.orderMissing', 'Order not found.') })
616
616
  }
617
617
  ensureSameScope(order, input.organizationId, input.tenantId)
618
- enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
618
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
619
619
 
620
620
  const orderLines = await findWithDecryption(
621
621
  tx,
@@ -890,7 +890,7 @@ const updateReturnCommand: CommandHandler<ReturnUpdateInput, { returnId: string
890
890
  }
891
891
  // Lock on the return's own version — editing header fields (reason / notes /
892
892
  // returnedAt) only touches the return, not the order totals.
893
- enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN)
893
+ await enforceSalesDocumentOptimisticLock(ctx, entity, SALES_RESOURCE_KIND_RETURN)
894
894
 
895
895
  if (input.reason !== undefined) entity.reason = input.reason.length ? input.reason : null
896
896
  if (input.notes !== undefined) entity.notes = input.notes.length ? input.notes : null
@@ -1020,7 +1020,7 @@ const deleteReturnCommand: CommandHandler<ReturnDeleteInput, { returnId: string
1020
1020
  }
1021
1021
  ensureSameScope(header, input.organizationId, input.tenantId)
1022
1022
  // Lock on the return's own version, captured before any mutation.
1023
- enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
1023
+ await enforceSalesDocumentOptimisticLock(ctx, header, SALES_RESOURCE_KIND_RETURN)
1024
1024
 
1025
1025
  await reverseReturnEffects(em, salesCalculationService, snapshot)
1026
1026
 
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
3
3
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
4
4
  import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
5
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
5
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
6
6
  export { assertFound } from '@open-mercato/shared/lib/crud/errors'
7
7
  export { ensureOrganizationScope, ensureSameScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
8
8
  export { extractUndoPayload } from '@open-mercato/shared/lib/commands/undo'
@@ -28,14 +28,20 @@ export const SALES_RESOURCE_KIND_RETURN = 'sales.return'
28
28
  *
29
29
  * Strictly additive: when the client sends no header the check is a no-op, so
30
30
  * existing API consumers are unaffected. Respects `OM_OPTIMISTIC_LOCK`.
31
+ *
32
+ * Routes through the async DI-aware seam `enforceCommandOptimisticLockWithGuards`
33
+ * (Phase 0 / S1): the OSS `updated_at` floor runs first (identical behavior when
34
+ * `record_locks` is disabled — floor only), then the optional enterprise
35
+ * `record_locks` enrichment is awaited so the aggregate check observes the
36
+ * action-log diff when the resource is enabled. Callers MUST `await` it.
31
37
  */
32
- export function enforceSalesDocumentOptimisticLock(
38
+ export async function enforceSalesDocumentOptimisticLock(
33
39
  ctx: CommandRuntimeContext,
34
40
  document: { id: string; updatedAt?: Date | string | null } | null | undefined,
35
41
  resourceKind: string,
36
- ): void {
42
+ ): Promise<void> {
37
43
  if (!document) return
38
- enforceCommandOptimisticLock({
44
+ await enforceCommandOptimisticLockWithGuards(ctx.container, {
39
45
  resourceKind,
40
46
  resourceId: document.id,
41
47
  current: document.updatedAt ?? null,
@@ -29,6 +29,8 @@ import {
29
29
  ensureSameScope,
30
30
  ensureTenantScope,
31
31
  extractUndoPayload,
32
+ enforceSalesDocumentOptimisticLock,
33
+ SALES_RESOURCE_KIND_ORDER,
32
34
  } from './shared'
33
35
  import { resolveDictionaryEntryValue } from '../lib/dictionaries'
34
36
  import { resolveRedoSnapshot } from '@open-mercato/shared/lib/commands/redo'
@@ -442,6 +444,10 @@ const createShipmentCommand: CommandHandler<ShipmentCreateInput, { shipmentId: s
442
444
  const shipment = await em.transactional(async (tx) => {
443
445
  const order = await loadOrder(tx, input.orderId, { tenantId: input.tenantId, organizationId: input.organizationId })
444
446
  ensureSameScope(order, input.organizationId, input.tenantId)
447
+ // Guard the parent order's aggregate version (Gap B): a shipment mutation
448
+ // recalculates the order's fulfilled quantities, so a stale parent must 409
449
+ // before we touch it.
450
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
445
451
  const { items: normalizedItems, lineMap } = await validateShipmentItems({
446
452
  em: tx,
447
453
  order,
@@ -674,6 +680,10 @@ const updateShipmentCommand: CommandHandler<ShipmentUpdateInput, { shipmentId: s
674
680
  if (input.orderId && input.orderId !== order.id) {
675
681
  throw new CrudHttpError(400, { error: 'sales.shipments.invalid_order' })
676
682
  }
683
+ // Guard the parent order's aggregate version (Gap B): updating a shipment
684
+ // recalculates the order's fulfilled quantities, so a stale parent must 409
685
+ // before we touch it.
686
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
677
687
 
678
688
  // Check if order is financially closed - prevent modifications to shipments
679
689
  const paidAmount = parseFloat(order.paidTotalAmount || '0')
@@ -976,6 +986,10 @@ const deleteShipmentCommand: CommandHandler<
976
986
  if (order.id !== payload.orderId) {
977
987
  throw new CrudHttpError(400, { error: translate('sales.shipments.invalid_order', 'Shipment does not belong to this order') })
978
988
  }
989
+ // Guard the parent order's aggregate version (Gap B): deleting a shipment
990
+ // recalculates the order's fulfilled quantities, so a stale parent must 409
991
+ // before we touch it.
992
+ await enforceSalesDocumentOptimisticLock(ctx, order, SALES_RESOURCE_KIND_ORDER)
979
993
  const scope = { tenantId: shipmentEntity.tenantId, organizationId: shipmentEntity.organizationId }
980
994
  const items = await findWithDecryption(tx, SalesShipmentItem, { shipment: shipmentEntity }, {}, scope)
981
995
  items.forEach((item) => tx.remove(item))
@@ -9,7 +9,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
9
9
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
10
10
  import { createCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
11
11
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
12
- import { handleSectionMutationError, rowOptimisticVersion } from './optimisticLock'
12
+ import { handleSectionMutationError } from './optimisticLock'
13
13
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
14
14
  import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
15
15
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -58,6 +58,7 @@ type PaymentDialogProps = {
58
58
  orderId: string
59
59
  organizationId: string | null
60
60
  tenantId: string | null
61
+ documentUpdatedAt?: string | null
61
62
  onOpenChange: (open: boolean) => void
62
63
  onSaved?: (totals?: PaymentTotals | null) => void | Promise<void>
63
64
  }
@@ -79,6 +80,7 @@ export function PaymentDialog({
79
80
  orderId,
80
81
  organizationId,
81
82
  tenantId,
83
+ documentUpdatedAt,
82
84
  onOpenChange,
83
85
  onSaved,
84
86
  }: PaymentDialogProps) {
@@ -539,7 +541,9 @@ export function PaymentDialog({
539
541
  const action = payment?.id ? updateCrud : createCrud
540
542
  try {
541
543
  const result = await withScopedApiRequestHeaders(
542
- buildOptimisticLockHeader(payment?.id ? rowOptimisticVersion(payment) : undefined),
544
+ // The server guards the PARENT order's aggregate version (Gap A) for
545
+ // both create and update, so send the order's `updated_at`.
546
+ buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
543
547
  () =>
544
548
  action(
545
549
  'sales/payments',
@@ -569,7 +573,7 @@ export function PaymentDialog({
569
573
  throw err
570
574
  }
571
575
  },
572
- [currencyCode, mode, onOpenChange, onSaved, orderId, organizationId, payment?.id, payment?.updatedAt, t, tenantId]
576
+ [currencyCode, documentUpdatedAt, mode, onOpenChange, onSaved, orderId, organizationId, payment?.id, t, tenantId]
573
577
  )
574
578
 
575
579
  const handleSubmitForm = React.useCallback(
@@ -7,7 +7,7 @@ import { LoadingMessage, ErrorMessage, TabEmptyState } from '@open-mercato/ui/ba
7
7
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
8
8
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
9
9
  import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
10
- import { handleSectionMutationError, readRowUpdatedAt, rowOptimisticVersion } from './optimisticLock'
10
+ import { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'
11
11
  import type { SectionAction } from '@open-mercato/ui/backend/detail'
12
12
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
13
13
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -41,6 +41,7 @@ type SalesDocumentPaymentsSectionProps = {
41
41
  currencyCode: string | null | undefined
42
42
  organizationId?: string | null
43
43
  tenantId?: string | null
44
+ documentUpdatedAt?: string | null
44
45
  onActionChange?: (action: SectionAction | null) => void
45
46
  onTotalsChange?: () => void
46
47
  onPaymentsChange?: (payments: PaymentRow[]) => void
@@ -69,6 +70,7 @@ export function SalesDocumentPaymentsSection({
69
70
  currencyCode,
70
71
  organizationId: orgFromProps,
71
72
  tenantId: tenantFromProps,
73
+ documentUpdatedAt,
72
74
  onActionChange,
73
75
  onTotalsChange,
74
76
  onPaymentsChange,
@@ -216,7 +218,9 @@ export function SalesDocumentPaymentsSection({
216
218
  async (row: PaymentRow) => {
217
219
  try {
218
220
  const result = await withScopedApiRequestHeaders(
219
- buildOptimisticLockHeader(rowOptimisticVersion(row)),
221
+ // The server guards the PARENT order's aggregate version (Gap A), so
222
+ // send the order's `updated_at`, not the payment row's.
223
+ buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
220
224
  () =>
221
225
  deleteCrud<{ orderTotals?: PaymentTotals | null }>('sales/payments', {
222
226
  body: {
@@ -242,7 +246,7 @@ export function SalesDocumentPaymentsSection({
242
246
  flash(t('sales.documents.payments.errorDelete', 'Failed to delete payment.'), 'error')
243
247
  }
244
248
  },
245
- [loadPayments, onTotalsChange, orderId, resolvedOrganizationId, resolvedTenantId, t]
249
+ [documentUpdatedAt, loadPayments, onTotalsChange, orderId, resolvedOrganizationId, resolvedTenantId, t]
246
250
  )
247
251
 
248
252
  React.useEffect(() => {
@@ -369,6 +373,7 @@ export function SalesDocumentPaymentsSection({
369
373
  orderId={orderId}
370
374
  organizationId={resolvedOrganizationId}
371
375
  tenantId={resolvedTenantId}
376
+ documentUpdatedAt={documentUpdatedAt ?? null}
372
377
  onSaved={handlePaymentSaved}
373
378
  />
374
379
  </div>
@@ -15,7 +15,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
15
15
  import { createCrud, updateCrud } from '@open-mercato/ui/backend/utils/crud'
16
16
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
17
17
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
18
- import { handleSectionMutationError, rowOptimisticVersion } from './optimisticLock'
18
+ import { handleSectionMutationError } from './optimisticLock'
19
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
20
20
  import { cn } from '@open-mercato/shared/lib/utils'
21
21
  import { E } from '#generated/entities.ids.generated'
@@ -46,6 +46,7 @@ type ShipmentDialogProps = {
46
46
  currencyCode?: string | null
47
47
  organizationId: string | null
48
48
  tenantId: string | null
49
+ documentUpdatedAt?: string | null
49
50
  computeAvailable: (lineId: string, excludeShipmentId?: string | null) => number
50
51
  shippingAddressSnapshot?: NormalizedAddressSnapshot | Record<string, unknown> | null
51
52
  onClose: () => void
@@ -278,6 +279,7 @@ export function ShipmentDialog({
278
279
  currencyCode,
279
280
  organizationId,
280
281
  tenantId,
282
+ documentUpdatedAt,
281
283
  computeAvailable,
282
284
  shippingAddressSnapshot,
283
285
  onClose,
@@ -1089,7 +1091,9 @@ export function ShipmentDialog({
1089
1091
  let result
1090
1092
  try {
1091
1093
  result = await withScopedApiRequestHeaders(
1092
- buildOptimisticLockHeader(shipment?.id ? rowOptimisticVersion(shipment) : undefined),
1094
+ // The server guards the PARENT order's aggregate version (Gap B) for
1095
+ // both create and update, so send the order's `updated_at`.
1096
+ buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
1093
1097
  () =>
1094
1098
  action(
1095
1099
  'sales/shipments',
@@ -1219,6 +1223,7 @@ export function ShipmentDialog({
1219
1223
  },
1220
1224
  [
1221
1225
  currencyCode,
1226
+ documentUpdatedAt,
1222
1227
  lines,
1223
1228
  mode,
1224
1229
  onAddComment,
@@ -1226,7 +1231,6 @@ export function ShipmentDialog({
1226
1231
  orderId,
1227
1232
  organizationId,
1228
1233
  shipment?.id,
1229
- shipment?.updatedAt,
1230
1234
  addressOptions,
1231
1235
  addressOptionsMap,
1232
1236
  shippingMethods,
@@ -20,7 +20,7 @@ import type { SectionAction } from '@open-mercato/core/modules/customers/compone
20
20
  import { generateTempId } from '@open-mercato/core/modules/customers/lib/detailHelpers'
21
21
  import { formatAddressString, type AddressValue } from '@open-mercato/core/modules/customers/utils/addressFormat'
22
22
  import { ShipmentDialog } from './ShipmentDialog'
23
- import { handleSectionMutationError, readRowUpdatedAt, rowOptimisticVersion } from './optimisticLock'
23
+ import { handleSectionMutationError, readRowUpdatedAt } from './optimisticLock'
24
24
  import { extractCustomFieldValues } from './customFieldHelpers'
25
25
  import type { OrderLine, ShipmentRow, ShipmentItem } from './shipmentTypes'
26
26
 
@@ -33,6 +33,7 @@ type SalesShipmentsSectionProps = {
33
33
  shippingAddressSnapshot?: Record<string, unknown> | null
34
34
  organizationId?: string | null
35
35
  tenantId?: string | null
36
+ documentUpdatedAt?: string | null
36
37
  onActionChange?: (action: SectionAction | null) => void
37
38
  onAddComment?: (body: string) => Promise<void>
38
39
  }
@@ -114,6 +115,7 @@ export function SalesShipmentsSection({
114
115
  shippingAddressSnapshot,
115
116
  organizationId: organizationIdProp,
116
117
  tenantId: tenantIdProp,
118
+ documentUpdatedAt,
117
119
  onActionChange,
118
120
  onAddComment,
119
121
  }: SalesShipmentsSectionProps) {
@@ -401,7 +403,9 @@ export function SalesShipmentsSection({
401
403
  if (!confirmed) return
402
404
  try {
403
405
  const result = await withScopedApiRequestHeaders(
404
- buildOptimisticLockHeader(rowOptimisticVersion(shipment)),
406
+ // The server guards the PARENT order's aggregate version (Gap B), so
407
+ // send the order's `updated_at`, not the shipment row's.
408
+ buildOptimisticLockHeader(documentUpdatedAt ?? undefined),
405
409
  () =>
406
410
  deleteCrud('sales/shipments', {
407
411
  body: {
@@ -425,7 +429,7 @@ export function SalesShipmentsSection({
425
429
  flash(t('sales.documents.shipments.errorDelete', 'Failed to delete shipment.'), 'error')
426
430
  }
427
431
  },
428
- [confirm, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]
432
+ [confirm, documentUpdatedAt, loadShipments, orderId, resolvedOrganizationId, resolvedTenantId, t]
429
433
  )
430
434
 
431
435
  const renderItemList = (items: ShipmentItem[]) => (
@@ -573,6 +577,7 @@ export function SalesShipmentsSection({
573
577
  currencyCode={currencyCode}
574
578
  organizationId={resolvedOrganizationId}
575
579
  tenantId={resolvedTenantId}
580
+ documentUpdatedAt={documentUpdatedAt ?? null}
576
581
  computeAvailable={computeAvailable}
577
582
  shippingAddressSnapshot={shippingAddressSnapshot}
578
583
  onClose={() => setDialogState(null)}
@@ -42,15 +42,20 @@ type AppCradle = AppContainer['cradle'] & {
42
42
  }
43
43
 
44
44
  const RESOURCE_KIND_ORDER = 'sales.order'
45
+ const RESOURCE_KIND_PAYMENT = 'sales.payment'
46
+ const RESOURCE_KIND_SHIPMENT = 'sales.shipment'
45
47
 
46
- const readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (
48
+ async function readOrderUpdatedAtById(
47
49
  em: EntityManager,
48
- { resourceId, tenantId, organizationId },
49
- ) => {
50
+ orderId: string | null | undefined,
51
+ tenantId: string,
52
+ organizationId: string | null | undefined,
53
+ ): Promise<string | null> {
54
+ if (!orderId) return null
50
55
  const row = await em.findOne(
51
56
  SalesOrder,
52
57
  {
53
- id: resourceId,
58
+ id: orderId,
54
59
  tenantId,
55
60
  ...(organizationId ? { organizationId } : {}),
56
61
  deletedAt: null,
@@ -60,13 +65,67 @@ const readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (
60
65
  return row?.updatedAt instanceof Date ? row.updatedAt.toISOString() : null
61
66
  }
62
67
 
63
- // Hand-wired sales.order reader registered at module-load time so the
64
- // `customer_entities`-style polymorphic-table override pattern stays
65
- // observable for downstream modules. Functionally identical to the
66
- // auto-registered generic reader; kept here as a reference example. The
67
- // guard's mode check short-circuits when `OM_OPTIMISTIC_LOCK=off`.
68
+ const readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (
69
+ em: EntityManager,
70
+ { resourceId, tenantId, organizationId },
71
+ ) => readOrderUpdatedAtById(em, resourceId, tenantId, organizationId)
72
+
73
+ // Payments and shipments are guarded against the PARENT ORDER's aggregate
74
+ // version (their mutations recalc order totals / fulfilled quantities, so the
75
+ // order is the consistency boundary — same model as lines/adjustments/returns).
76
+ // The CRUD-layer guard therefore must compare against the order, not the
77
+ // payment/shipment row: resolve the parent order from the sub-resource id and
78
+ // return its `updated_at`. This keeps the single optimistic-lock header
79
+ // consistent across the makeCrudRoute guard and the command-level
80
+ // `enforceSalesDocumentOptimisticLock` guard (both read the order version).
81
+ const readPaymentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (
82
+ em: EntityManager,
83
+ { resourceId, tenantId, organizationId },
84
+ ) => {
85
+ const payment = await em.findOne(
86
+ SalesPayment,
87
+ {
88
+ id: resourceId,
89
+ tenantId,
90
+ ...(organizationId ? { organizationId } : {}),
91
+ deletedAt: null,
92
+ },
93
+ { fields: ['order'] as const },
94
+ )
95
+ const orderId = (payment as { order?: { id?: string } | string | null } | null)?.order
96
+ const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null
97
+ return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)
98
+ }
99
+
100
+ const readShipmentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (
101
+ em: EntityManager,
102
+ { resourceId, tenantId, organizationId },
103
+ ) => {
104
+ const shipment = await em.findOne(
105
+ SalesShipment,
106
+ {
107
+ id: resourceId,
108
+ tenantId,
109
+ ...(organizationId ? { organizationId } : {}),
110
+ deletedAt: null,
111
+ },
112
+ { fields: ['order'] as const },
113
+ )
114
+ const orderId = (shipment as { order?: { id?: string } | string | null } | null)?.order
115
+ const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null
116
+ return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)
117
+ }
118
+
119
+ // Hand-wired sales readers registered at module-load time. `sales.order` mirrors
120
+ // the auto-registered generic reader (kept as the polymorphic-table override
121
+ // reference). `sales.payment`/`sales.shipment` redirect the CRUD-layer guard to
122
+ // the parent order so payments/shipments guard the aggregate (Phase 3 Gap A/B)
123
+ // instead of their own row. The guard's mode check short-circuits when
124
+ // `OM_OPTIMISTIC_LOCK=off`.
68
125
  registerOptimisticLockReaders({
69
126
  [RESOURCE_KIND_ORDER]: readSalesOrderUpdatedAt,
127
+ [RESOURCE_KIND_PAYMENT]: readPaymentParentOrderUpdatedAt,
128
+ [RESOURCE_KIND_SHIPMENT]: readShipmentParentOrderUpdatedAt,
70
129
  })
71
130
 
72
131
  export function register(container: AppContainer) {
@@ -51,7 +51,9 @@ export const setup: ModuleSetupConfig = {
51
51
  admin: ['sales.*', 'sales.documents.number.edit'],
52
52
  employee: [
53
53
  'sales.channels.view',
54
+ 'sales.channels.manage',
54
55
  'sales.settings.view',
56
+ 'sales.settings.manage',
55
57
  'sales.orders.view',
56
58
  'sales.orders.manage',
57
59
  'sales.orders.approve',
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { usePathname, useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { Badge } from '@open-mercato/ui/primitives/badge'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -14,12 +14,14 @@ import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
14
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
16
  import { LeaveRequestForm, buildLeaveRequestPayload, type LeaveRequestFormValues } from '@open-mercato/core/modules/staff/components/LeaveRequestForm'
17
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
17
18
  import { type LeaveRequestRecord, type LeaveRequestsResponse, type NormalizedLeaveRequest, normalizeLeaveRequest, resolveStatusVariant, formatDateLabel, formatDateRange } from '../../../../lib/leaveRequestHelpers'
18
19
 
19
20
  export default function StaffLeaveRequestDetailPage({ params }: { params?: { id?: string } }) {
20
21
  const id = params?.id
21
22
  const t = useT()
22
23
  const router = useRouter()
24
+ const pathname = usePathname()
23
25
  const [isLoading, setIsLoading] = React.useState(true)
24
26
  const [error, setError] = React.useState<string | null>(null)
25
27
  const [isNotFound, setIsNotFound] = React.useState(false)
@@ -123,6 +125,20 @@ const handleSubmit = React.useCallback(async (values: LeaveRequestFormValues) =>
123
125
  router.refresh()
124
126
  }, [decisionComment, record?.id, record?.updatedAt, router, t])
125
127
 
128
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
129
+ // mount so the enterprise record_locks widget resolves `staff.leaveRequest` + id
130
+ // explicitly. The resourceKind follows the module's camelCase version-history
131
+ // convention so the held lock matches the leave request's save-time conflict surface.
132
+ useSetCurrentRecordInjectionContext(
133
+ buildRecordInjectionContext({
134
+ resourceKind: 'staff.leaveRequest',
135
+ resourceId: id || null,
136
+ updatedAt: record?.updatedAt ?? null,
137
+ data: record as Record<string, unknown> | null,
138
+ path: pathname,
139
+ }),
140
+ )
141
+
126
142
  if (isLoading) {
127
143
  return (
128
144
  <Page>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useRouter, useSearchParams } from 'next/navigation'
5
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
8
  import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
@@ -15,6 +15,7 @@ import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/tran
15
15
  import { AvailabilityRulesEditor } from '@open-mercato/core/modules/planner/components/AvailabilityRulesEditor'
16
16
  import { buildMemberScheduleItems } from '@open-mercato/core/modules/staff/lib/memberSchedule'
17
17
  import { TeamMemberForm, buildTeamMemberPayload, type TeamMemberFormValues } from '@open-mercato/core/modules/staff/components/TeamMemberForm'
18
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
18
19
  import { NotesSection } from '@open-mercato/ui/backend/detail'
19
20
  import { ActivitiesSection, type SectionAction } from '@open-mercato/ui/backend/detail'
20
21
  import { AddressesSection as SharedAddressesSection } from '@open-mercato/ui/backend/detail'
@@ -71,6 +72,7 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
71
72
  const t = useT()
72
73
  const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
73
74
  const router = useRouter()
75
+ const pathname = usePathname()
74
76
  const searchParams = useSearchParams()
75
77
  const [initialValues, setInitialValues] = React.useState<TeamMemberFormValues | null>(null)
76
78
  const [memberRecord, setMemberRecord] = React.useState<TeamMemberRecord | null>(null)
@@ -341,6 +343,20 @@ export default function StaffTeamMemberDetailPage({ params }: { params?: { id?:
341
343
  : [t('staff.teamMembers.detail.roles.unassigned', 'No roles assigned')]
342
344
  const userEmail = memberRecord?.user?.email ?? null
343
345
 
346
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
347
+ // mount so the enterprise record_locks widget resolves `staff.teamMember` + id
348
+ // explicitly. The resourceKind mirrors the TeamMemberForm `versionHistory` so the held
349
+ // lock matches the save-time conflict surface for the same team member.
350
+ useSetCurrentRecordInjectionContext(
351
+ buildRecordInjectionContext({
352
+ resourceKind: 'staff.teamMember',
353
+ resourceId: memberId || null,
354
+ updatedAt: initialValues?.updatedAt ?? null,
355
+ data: initialValues as Record<string, unknown> | null,
356
+ path: pathname,
357
+ }),
358
+ )
359
+
344
360
  if (isNotFound) {
345
361
  return (
346
362
  <Page>
@@ -1,9 +1,10 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { usePathname, useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
7
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
7
8
  import { readApiResultOrThrow, apiCall } from '@open-mercato/ui/backend/utils/apiCall'
8
9
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
9
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
@@ -40,6 +41,7 @@ export default function StaffTeamRoleEditPage({ params }: { params?: { id?: stri
40
41
  const roleId = params?.id
41
42
  const t = useT()
42
43
  const router = useRouter()
44
+ const pathname = usePathname()
43
45
  const scopeVersion = useOrganizationScopeVersion()
44
46
  // optimistic-lock: TeamRoleForm forwards optimisticLockUpdatedAt from initialValues.updatedAt (wrapper auto-derives the header on save + delete).
45
47
  const [initialValues, setInitialValues] = React.useState<TeamRoleFormValues | null>(null)
@@ -202,6 +204,20 @@ export default function StaffTeamRoleEditPage({ params }: { params?: { id?: stri
202
204
  router.push('/backend/staff/team-roles')
203
205
  }, [roleId, router, t])
204
206
 
207
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
208
+ // mount so the enterprise record_locks widget resolves `staff.teamRole` + id
209
+ // explicitly. The resourceKind mirrors the TeamRoleForm `versionHistory` so the held
210
+ // lock matches the save-time conflict surface for the same team role.
211
+ useSetCurrentRecordInjectionContext(
212
+ buildRecordInjectionContext({
213
+ resourceKind: 'staff.teamRole',
214
+ resourceId: roleId || null,
215
+ updatedAt: initialValues?.updatedAt ?? null,
216
+ data: initialValues as Record<string, unknown> | null,
217
+ path: pathname,
218
+ }),
219
+ )
220
+
205
221
  if (isNotFound) {
206
222
  return (
207
223
  <Page>
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import Link from 'next/link'
5
- import { useRouter, useSearchParams } from 'next/navigation'
5
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
6
6
  import type { ColumnDef, SortingState } from '@tanstack/react-table'
7
7
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
8
8
  import { ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
@@ -16,6 +16,7 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
16
16
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
17
17
  import { useT } from '@open-mercato/shared/lib/i18n/context'
18
18
  import { TeamForm, type TeamFormValues, buildTeamPayload } from '@open-mercato/core/modules/staff/components/TeamForm'
19
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
19
20
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
20
21
  import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
21
22
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -60,6 +61,7 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
60
61
  const teamId = params?.id
61
62
  const t = useT()
62
63
  const router = useRouter()
64
+ const pathname = usePathname()
63
65
  const searchParams = useSearchParams()
64
66
  const scopeVersion = useOrganizationScopeVersion()
65
67
  const [initialValues, setInitialValues] = React.useState<TeamFormValues | null>(null)
@@ -315,6 +317,20 @@ export default function StaffTeamEditPage({ params }: { params?: { id?: string }
315
317
  router.push('/backend/staff/teams')
316
318
  }, [teamId, router, t])
317
319
 
320
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
321
+ // mount so the enterprise record_locks widget resolves `staff.team` + id explicitly.
322
+ // The resourceKind mirrors the TeamForm `versionHistory` so the held lock matches
323
+ // the save-time conflict surface for the same team.
324
+ useSetCurrentRecordInjectionContext(
325
+ buildRecordInjectionContext({
326
+ resourceKind: 'staff.team',
327
+ resourceId: teamId || null,
328
+ updatedAt: initialValues?.updatedAt ?? null,
329
+ data: initialValues as Record<string, unknown> | null,
330
+ path: pathname,
331
+ }),
332
+ )
333
+
318
334
  if (isNotFound) {
319
335
  return (
320
336
  <Page>