@open-mercato/core 0.6.6-develop.6330.1.a261878aa8 → 0.6.6-develop.6332.1.6e73f0f55b

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 (253) 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/logout.js +2 -3
  4. package/dist/modules/auth/api/logout.js.map +2 -2
  5. package/dist/modules/auth/api/roles/acl/route.js +2 -2
  6. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  7. package/dist/modules/auth/api/session/refresh.js +5 -6
  8. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  9. package/dist/modules/auth/api/users/acl/route.js +2 -2
  10. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  11. package/dist/modules/auth/backend/roles/[id]/edit/page.js +12 -0
  12. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +12 -0
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/data/entities.js +2 -2
  16. package/dist/modules/auth/data/entities.js.map +2 -2
  17. package/dist/modules/auth/lib/requestRedirect.js +31 -3
  18. package/dist/modules/auth/lib/requestRedirect.js.map +2 -2
  19. package/dist/modules/business_rules/api/rules/route.js +3 -3
  20. package/dist/modules/business_rules/api/rules/route.js.map +2 -2
  21. package/dist/modules/business_rules/api/sets/route.js +3 -3
  22. package/dist/modules/business_rules/api/sets/route.js.map +2 -2
  23. package/dist/modules/business_rules/backend/rules/[id]/page.js +12 -1
  24. package/dist/modules/business_rules/backend/rules/[id]/page.js.map +2 -2
  25. package/dist/modules/business_rules/backend/sets/[id]/page.js +12 -1
  26. package/dist/modules/business_rules/backend/sets/[id]/page.js.map +2 -2
  27. package/dist/modules/catalog/api/product-media/route.js.map +2 -2
  28. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js +12 -0
  29. package/dist/modules/catalog/backend/catalog/categories/[id]/edit/page.js.map +2 -2
  30. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +15 -0
  31. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  32. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +12 -1
  33. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  34. package/dist/modules/catalog/components/PriceKindSettings.js +13 -3
  35. package/dist/modules/catalog/components/PriceKindSettings.js.map +2 -2
  36. package/dist/modules/catalog/lib/bulkDelete.js.map +2 -2
  37. package/dist/modules/currencies/backend/currencies/[id]/page.js +15 -3
  38. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  39. package/dist/modules/currencies/backend/currencies/page.js +2 -1
  40. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  41. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js +12 -1
  42. package/dist/modules/currencies/backend/exchange-rates/[id]/page.js.map +2 -2
  43. package/dist/modules/currencies/backend/exchange-rates/page.js +3 -2
  44. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  45. package/dist/modules/customer_accounts/api/admin/roles/[id].js +3 -3
  46. package/dist/modules/customer_accounts/api/admin/roles/[id].js.map +2 -2
  47. package/dist/modules/customer_accounts/api/admin/users/[id].js +3 -3
  48. package/dist/modules/customer_accounts/api/admin/users/[id].js.map +2 -2
  49. package/dist/modules/customers/api/addresses/route.js +4 -2
  50. package/dist/modules/customers/api/addresses/route.js.map +2 -2
  51. package/dist/modules/customers/api/tags/route.js +8 -3
  52. package/dist/modules/customers/api/tags/route.js.map +2 -2
  53. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +13 -2
  54. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  55. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +24 -9
  56. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
  57. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js +53 -23
  58. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.js.map +2 -2
  59. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js +13 -3
  60. package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.js.map +2 -2
  61. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -2
  62. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  63. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +13 -2
  64. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  65. package/dist/modules/customers/commands/interactions.js +5 -5
  66. package/dist/modules/customers/commands/interactions.js.map +2 -2
  67. package/dist/modules/customers/commands/pipeline-stages.js +24 -2
  68. package/dist/modules/customers/commands/pipeline-stages.js.map +2 -2
  69. package/dist/modules/customers/commands/pipelines.js +24 -2
  70. package/dist/modules/customers/commands/pipelines.js.map +2 -2
  71. package/dist/modules/customers/components/detail/AddressesSection.js +46 -29
  72. package/dist/modules/customers/components/detail/AddressesSection.js.map +2 -2
  73. package/dist/modules/customers/components/detail/DealForm.js +5 -1
  74. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  75. package/dist/modules/customers/components/detail/notesAdapter.js +25 -18
  76. package/dist/modules/customers/components/detail/notesAdapter.js.map +2 -2
  77. package/dist/modules/data_sync/api/schedules/[id]/route.js +38 -19
  78. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  79. package/dist/modules/data_sync/api/schedules/route.js +1 -1
  80. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  81. package/dist/modules/data_sync/backend/data-sync/page.js +32 -15
  82. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  83. package/dist/modules/data_sync/lib/sync-schedule-service.js +19 -5
  84. package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
  85. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js +3 -3
  86. package/dist/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.js.map +2 -2
  87. package/dist/modules/dictionaries/api/[dictionaryId]/route.js +3 -3
  88. package/dist/modules/dictionaries/api/[dictionaryId]/route.js.map +2 -2
  89. package/dist/modules/dictionaries/components/DictionariesManager.js +12 -1
  90. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  91. package/dist/modules/dictionaries/components/DictionaryEntriesEditor.js.map +2 -2
  92. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js +12 -0
  93. package/dist/modules/directory/backend/directory/organizations/[id]/edit/page.js.map +2 -2
  94. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js +12 -0
  95. package/dist/modules/directory/backend/directory/tenants/[id]/edit/page.js.map +2 -2
  96. package/dist/modules/entities/api/entities.js +2 -2
  97. package/dist/modules/entities/api/entities.js.map +2 -2
  98. package/dist/modules/entities/api/records.js +7 -5
  99. package/dist/modules/entities/api/records.js.map +2 -2
  100. package/dist/modules/feature_toggles/api/overrides/route.js +2 -2
  101. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  102. package/dist/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.js.map +2 -2
  103. package/dist/modules/feature_toggles/data/entities.js +1 -1
  104. package/dist/modules/feature_toggles/data/entities.js.map +2 -2
  105. package/dist/modules/feature_toggles/data/validators.js +2 -1
  106. package/dist/modules/feature_toggles/data/validators.js.map +2 -2
  107. package/dist/modules/feature_toggles/lib/queries.js +2 -1
  108. package/dist/modules/feature_toggles/lib/queries.js.map +2 -2
  109. package/dist/modules/inbox_ops/api/settings/route.js +2 -2
  110. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  111. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +12 -1
  112. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  113. package/dist/modules/resources/backend/resources/resources/[id]/page.js +12 -1
  114. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  115. package/dist/modules/resources/components/detail/notesAdapter.js +25 -18
  116. package/dist/modules/resources/components/detail/notesAdapter.js.map +2 -2
  117. package/dist/modules/sales/backend/sales/documents/[id]/page.js +14 -1
  118. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  119. package/dist/modules/sales/commands/documents.js +11 -10
  120. package/dist/modules/sales/commands/documents.js.map +2 -2
  121. package/dist/modules/sales/commands/payments.js +6 -1
  122. package/dist/modules/sales/commands/payments.js.map +2 -2
  123. package/dist/modules/sales/commands/returns.js +3 -3
  124. package/dist/modules/sales/commands/returns.js.map +2 -2
  125. package/dist/modules/sales/commands/shared.js +3 -3
  126. package/dist/modules/sales/commands/shared.js.map +2 -2
  127. package/dist/modules/sales/commands/shipments.js +6 -1
  128. package/dist/modules/sales/commands/shipments.js.map +2 -2
  129. package/dist/modules/sales/components/documents/PaymentDialog.js +6 -3
  130. package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
  131. package/dist/modules/sales/components/documents/PaymentsSection.js +7 -3
  132. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  133. package/dist/modules/sales/components/documents/ShipmentDialog.js +6 -3
  134. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  135. package/dist/modules/sales/components/documents/ShipmentsSection.js +7 -3
  136. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  137. package/dist/modules/sales/di.js +39 -3
  138. package/dist/modules/sales/di.js.map +2 -2
  139. package/dist/modules/sales/setup.js +2 -0
  140. package/dist/modules/sales/setup.js.map +2 -2
  141. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js +12 -1
  142. package/dist/modules/staff/backend/staff/leave-requests/[id]/page.js.map +2 -2
  143. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +12 -1
  144. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  145. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js +12 -1
  146. package/dist/modules/staff/backend/staff/team-roles/[id]/edit/page.js.map +2 -2
  147. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js +12 -1
  148. package/dist/modules/staff/backend/staff/teams/[id]/edit/page.js.map +2 -2
  149. package/dist/modules/staff/commands/job-histories.js +3 -3
  150. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  151. package/dist/modules/staff/components/detail/addressesAdapter.js +33 -24
  152. package/dist/modules/staff/components/detail/addressesAdapter.js.map +2 -2
  153. package/dist/modules/staff/components/detail/notesAdapter.js +25 -18
  154. package/dist/modules/staff/components/detail/notesAdapter.js.map +2 -2
  155. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +37 -0
  156. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  157. package/dist/modules/translations/components/TranslationManager.js +7 -1
  158. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  159. package/dist/modules/workflows/api/definitions/[id]/route.js +3 -3
  160. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  161. package/dist/modules/workflows/backend/definitions/[id]/page.js +12 -1
  162. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  163. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +11 -1
  164. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  165. package/package.json +7 -7
  166. package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +3 -0
  167. package/src/modules/auth/api/logout.ts +2 -3
  168. package/src/modules/auth/api/roles/acl/route.ts +2 -2
  169. package/src/modules/auth/api/session/refresh.ts +5 -6
  170. package/src/modules/auth/api/users/acl/route.ts +2 -2
  171. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +17 -0
  172. package/src/modules/auth/backend/users/[id]/edit/page.tsx +17 -0
  173. package/src/modules/auth/data/entities.ts +2 -2
  174. package/src/modules/auth/lib/requestRedirect.ts +44 -2
  175. package/src/modules/business_rules/api/rules/route.ts +3 -3
  176. package/src/modules/business_rules/api/sets/route.ts +3 -3
  177. package/src/modules/business_rules/backend/rules/[id]/page.tsx +17 -1
  178. package/src/modules/business_rules/backend/sets/[id]/page.tsx +18 -1
  179. package/src/modules/catalog/api/product-media/route.ts +4 -0
  180. package/src/modules/catalog/backend/catalog/categories/[id]/edit/page.tsx +17 -0
  181. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -0
  182. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +16 -1
  183. package/src/modules/catalog/components/PriceKindSettings.tsx +15 -7
  184. package/src/modules/catalog/lib/bulkDelete.ts +6 -0
  185. package/src/modules/currencies/backend/currencies/[id]/page.tsx +20 -3
  186. package/src/modules/currencies/backend/currencies/page.tsx +2 -1
  187. package/src/modules/currencies/backend/exchange-rates/[id]/page.tsx +17 -1
  188. package/src/modules/currencies/backend/exchange-rates/page.tsx +3 -2
  189. package/src/modules/customer_accounts/api/admin/roles/[id].ts +3 -3
  190. package/src/modules/customer_accounts/api/admin/users/[id].ts +3 -3
  191. package/src/modules/customers/api/addresses/route.ts +2 -0
  192. package/src/modules/customers/api/tags/route.ts +6 -1
  193. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +19 -2
  194. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +29 -8
  195. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealClosure.ts +47 -20
  196. package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealPipeline.ts +13 -3
  197. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  198. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -2
  199. package/src/modules/customers/commands/interactions.ts +5 -5
  200. package/src/modules/customers/commands/pipeline-stages.ts +26 -2
  201. package/src/modules/customers/commands/pipelines.ts +26 -2
  202. package/src/modules/customers/components/detail/AddressesSection.tsx +50 -28
  203. package/src/modules/customers/components/detail/DealForm.tsx +12 -0
  204. package/src/modules/customers/components/detail/notesAdapter.ts +29 -18
  205. package/src/modules/customers/i18n/de.json +2 -0
  206. package/src/modules/customers/i18n/en.json +2 -0
  207. package/src/modules/customers/i18n/es.json +2 -0
  208. package/src/modules/customers/i18n/pl.json +2 -0
  209. package/src/modules/data_sync/api/schedules/[id]/route.ts +38 -20
  210. package/src/modules/data_sync/api/schedules/route.ts +1 -1
  211. package/src/modules/data_sync/backend/data-sync/page.tsx +32 -15
  212. package/src/modules/data_sync/lib/sync-schedule-service.ts +30 -5
  213. package/src/modules/dictionaries/api/[dictionaryId]/entries/[entryId]/route.ts +3 -3
  214. package/src/modules/dictionaries/api/[dictionaryId]/route.ts +3 -3
  215. package/src/modules/dictionaries/components/DictionariesManager.tsx +21 -1
  216. package/src/modules/dictionaries/components/DictionaryEntriesEditor.tsx +2 -0
  217. package/src/modules/directory/backend/directory/organizations/[id]/edit/page.tsx +17 -0
  218. package/src/modules/directory/backend/directory/tenants/[id]/edit/page.tsx +17 -0
  219. package/src/modules/entities/api/entities.ts +2 -2
  220. package/src/modules/entities/api/records.ts +7 -5
  221. package/src/modules/feature_toggles/api/overrides/route.ts +2 -2
  222. package/src/modules/feature_toggles/backend/feature-toggles/global/[id]/edit/page.tsx +12 -0
  223. package/src/modules/feature_toggles/data/entities.ts +1 -1
  224. package/src/modules/feature_toggles/data/validators.ts +1 -0
  225. package/src/modules/feature_toggles/lib/queries.ts +1 -0
  226. package/src/modules/inbox_ops/api/settings/route.ts +2 -2
  227. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +17 -1
  228. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +17 -1
  229. package/src/modules/resources/components/detail/notesAdapter.ts +29 -18
  230. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -1
  231. package/src/modules/sales/commands/documents.ts +11 -10
  232. package/src/modules/sales/commands/payments.ts +11 -0
  233. package/src/modules/sales/commands/returns.ts +3 -3
  234. package/src/modules/sales/commands/shared.ts +10 -4
  235. package/src/modules/sales/commands/shipments.ts +14 -0
  236. package/src/modules/sales/components/documents/PaymentDialog.tsx +7 -3
  237. package/src/modules/sales/components/documents/PaymentsSection.tsx +8 -3
  238. package/src/modules/sales/components/documents/ShipmentDialog.tsx +7 -3
  239. package/src/modules/sales/components/documents/ShipmentsSection.tsx +8 -3
  240. package/src/modules/sales/di.ts +68 -9
  241. package/src/modules/sales/setup.ts +2 -0
  242. package/src/modules/staff/backend/staff/leave-requests/[id]/page.tsx +17 -1
  243. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +17 -1
  244. package/src/modules/staff/backend/staff/team-roles/[id]/edit/page.tsx +17 -1
  245. package/src/modules/staff/backend/staff/teams/[id]/edit/page.tsx +17 -1
  246. package/src/modules/staff/commands/job-histories.ts +3 -3
  247. package/src/modules/staff/components/detail/addressesAdapter.ts +40 -23
  248. package/src/modules/staff/components/detail/notesAdapter.ts +28 -18
  249. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +74 -0
  250. package/src/modules/translations/components/TranslationManager.tsx +14 -1
  251. package/src/modules/workflows/api/definitions/[id]/route.ts +3 -3
  252. package/src/modules/workflows/backend/definitions/[id]/page.tsx +18 -1
  253. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +19 -1
@@ -1,14 +1,16 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter, useParams } from 'next/navigation'
4
+ import { useRouter, useParams, usePathname } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { CrudForm, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
7
7
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
8
8
  import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
11
- import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
11
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
12
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
13
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
12
14
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
13
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
14
16
  import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
@@ -35,6 +37,7 @@ type CurrencyData = {
35
37
  export default function EditCurrencyPage({ params }: { params?: { id?: string } }) {
36
38
  const t = useT()
37
39
  const router = useRouter()
40
+ const pathname = usePathname()
38
41
  const { confirm: confirmDialog, ConfirmDialogElement } = useConfirmDialog()
39
42
  const mutationContextId = 'currencies-edit:delete'
40
43
  const { runMutation, retryLastMutation } = useGuardedMutation<{
@@ -185,11 +188,25 @@ export default function EditCurrencyPage({ params }: { params?: { id?: string }
185
188
  flash(t('currencies.flash.deleted'), 'success')
186
189
  router.push('/backend/currencies')
187
190
  } catch (error) {
188
- if (extractOptimisticLockConflict(error)) return
191
+ if (surfaceRecordConflict(error, t)) return
189
192
  flash(t('currencies.flash.deleteError'), 'error')
190
193
  }
191
194
  }, [currency, t, router, confirmDialog, mutationContextId, retryLastMutation, runMutation])
192
195
 
196
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
197
+ // mount so the enterprise record_locks widget resolves `currencies.currency` + id
198
+ // explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the held
199
+ // lock matches the save-time conflict surface for the same currency.
200
+ useSetCurrentRecordInjectionContext(
201
+ buildRecordInjectionContext({
202
+ resourceKind: 'currencies.currency',
203
+ resourceId: currency?.id ?? null,
204
+ updatedAt: currency?.updatedAt ?? currency?.updated_at ?? null,
205
+ data: currency as Record<string, unknown> | null,
206
+ path: pathname,
207
+ }),
208
+ )
209
+
193
210
  if (loading) {
194
211
  return (
195
212
  <Page>
@@ -14,6 +14,7 @@ import { Plus, Star } from 'lucide-react'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
15
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
16
16
  import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
17
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
17
18
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
18
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
19
20
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -188,7 +189,7 @@ export default function CurrenciesPage() {
188
189
  flash(t('currencies.flash.deleted'), 'success')
189
190
  setReloadToken((token) => token + 1)
190
191
  } catch (error) {
191
- if (extractOptimisticLockConflict(error)) return
192
+ if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
192
193
  flash(t('currencies.flash.deleteError'), 'error')
193
194
  }
194
195
  },
@@ -1,13 +1,14 @@
1
1
  'use client'
2
2
 
3
3
  import * as React from 'react'
4
- import { useRouter } from 'next/navigation'
4
+ import { useRouter, usePathname } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
9
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
10
10
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
11
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
11
12
  import { useT } from '@open-mercato/shared/lib/i18n/context'
12
13
  import {
13
14
  loadCurrencyOptions,
@@ -47,6 +48,7 @@ type ExchangeRateData = {
47
48
  export default function EditExchangeRatePage({ params }: { params?: { id?: string } }) {
48
49
  const t = useT()
49
50
  const router = useRouter()
51
+ const pathname = usePathname()
50
52
 
51
53
  const [exchangeRate, setExchangeRate] = React.useState<ExchangeRateData | null>(null)
52
54
  const [loading, setLoading] = React.useState(true)
@@ -84,6 +86,20 @@ export default function EditExchangeRatePage({ params }: { params?: { id?: strin
84
86
  [t, loadOptions]
85
87
  )
86
88
 
89
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
90
+ // mount so the enterprise record_locks widget resolves `currencies.exchange_rate`
91
+ // + id explicitly. The resourceKind mirrors the CrudForm `versionHistory` so the
92
+ // held lock matches the save-time conflict surface for the same exchange rate.
93
+ useSetCurrentRecordInjectionContext(
94
+ buildRecordInjectionContext({
95
+ resourceKind: 'currencies.exchange_rate',
96
+ resourceId: exchangeRate?.id ?? null,
97
+ updatedAt: exchangeRate?.updatedAt ?? exchangeRate?.updated_at ?? null,
98
+ data: exchangeRate as Record<string, unknown> | null,
99
+ path: pathname,
100
+ }),
101
+ )
102
+
87
103
  if (loading) {
88
104
  return (
89
105
  <Page>
@@ -13,7 +13,8 @@ import { BooleanIcon } from '@open-mercato/ui/backend/ValueIcons'
13
13
  import { Plus } from 'lucide-react'
14
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
15
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
16
- import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
16
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
17
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
17
18
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
18
19
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
19
20
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
@@ -152,7 +153,7 @@ export default function ExchangeRatesPage() {
152
153
  flash(t('exchangeRates.flash.deleted'), 'success')
153
154
  setReloadToken((token) => token + 1)
154
155
  } catch (error) {
155
- if (extractOptimisticLockConflict(error)) return
156
+ if (surfaceRecordConflict(error, t, { onRefresh: () => setReloadToken((token) => token + 1) })) return
156
157
  flash(t('exchangeRates.flash.deleteError'), 'error')
157
158
  }
158
159
  },
@@ -7,7 +7,7 @@ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacServic
7
7
  import { CustomerRole, CustomerRoleAcl, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'
8
8
  import { updateRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
9
9
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
10
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
10
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
11
11
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
12
12
 
13
13
  export const metadata = {}
@@ -107,7 +107,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
107
107
  // Optimistic lock: refuse a stale overwrite so two admins editing the same role
108
108
  // in parallel cannot silently clobber each other (#2055). Strictly additive.
109
109
  try {
110
- enforceCommandOptimisticLock({
110
+ await enforceCommandOptimisticLockWithGuards(container, {
111
111
  resourceKind: 'customer_accounts.role',
112
112
  resourceId: role.id,
113
113
  current: role.updatedAt ?? null,
@@ -169,7 +169,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
169
169
 
170
170
  // Optimistic lock: refuse a stale delete. Strictly additive.
171
171
  try {
172
- enforceCommandOptimisticLock({
172
+ await enforceCommandOptimisticLockWithGuards(container, {
173
173
  resourceKind: 'customer_accounts.role',
174
174
  resourceId: role.id,
175
175
  current: role.updatedAt ?? null,
@@ -12,7 +12,7 @@ import { adminUpdateUserSchema } from '@open-mercato/core/modules/customer_accou
12
12
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
13
13
  import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
14
14
  import { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
15
- import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
15
+ import { enforceCommandOptimisticLockWithGuards } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
16
16
  import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
17
17
 
18
18
  export const metadata = {}
@@ -143,7 +143,7 @@ export async function PUT(req: Request, { params }: { params: { id: string } })
143
143
  // customer user in parallel cannot silently clobber each other (#2055). The
144
144
  // check is strictly additive — a no-op when the client sends no expected-version header.
145
145
  try {
146
- enforceCommandOptimisticLock({
146
+ await enforceCommandOptimisticLockWithGuards(container, {
147
147
  resourceKind: 'customer_accounts.user',
148
148
  resourceId: user.id,
149
149
  current: user.updatedAt ?? null,
@@ -267,7 +267,7 @@ export async function DELETE(req: Request, { params }: { params: { id: string }
267
267
  // Optimistic lock: refuse a stale delete (e.g. deleting a record another admin
268
268
  // already modified). Strictly additive — a no-op without the expected-version header.
269
269
  try {
270
- enforceCommandOptimisticLock({
270
+ await enforceCommandOptimisticLockWithGuards(container, {
271
271
  resourceKind: 'customer_accounts.user',
272
272
  resourceId: user.id,
273
273
  current: user.updatedAt ?? null,
@@ -68,6 +68,7 @@ const crud = makeCrudRoute({
68
68
  'is_primary',
69
69
  'organization_id',
70
70
  'tenant_id',
71
+ 'updated_at',
71
72
  ],
72
73
  sortFieldMap: {
73
74
  createdAt: 'created_at',
@@ -142,6 +143,7 @@ const addressListItemSchema = z.object({
142
143
  is_primary: z.boolean().nullable().optional(),
143
144
  organization_id: z.string().uuid().nullable().optional(),
144
145
  tenant_id: z.string().uuid().nullable().optional(),
146
+ updated_at: z.string().nullable().optional(),
145
147
  })
146
148
 
147
149
  const addressCreateResponseSchema = z.object({
@@ -42,12 +42,16 @@ const crud = makeCrudRoute({
42
42
  idField: 'id',
43
43
  orgField: 'organizationId',
44
44
  tenantField: 'tenantId',
45
+ // CustomerTag has no `deleted_at` column. Without this, the auto-registered
46
+ // generic optimistic-lock reader would filter on `deletedAt: null` against a
47
+ // non-existent column, throw, and fail open — silently disabling the lock.
48
+ softDeleteField: null,
45
49
  },
46
50
  indexer: { entityType: E.customers.customer_tag },
47
51
  list: {
48
52
  schema: listSchema,
49
53
  entityId: E.customers.customer_tag,
50
- fields: ['id', 'slug', 'label', 'color', 'description', 'organization_id', 'tenant_id'],
54
+ fields: ['id', 'slug', 'label', 'color', 'description', 'organization_id', 'tenant_id', 'updated_at'],
51
55
  buildFilters: async (query: any) => {
52
56
  const filters: Record<string, any> = {}
53
57
  if (query.search) {
@@ -107,6 +111,7 @@ const tagListItemSchema = z.object({
107
111
  description: z.string().nullable().optional(),
108
112
  organization_id: z.string().uuid().nullable().optional(),
109
113
  tenant_id: z.string().uuid().nullable().optional(),
114
+ updated_at: z.string().nullable().optional(),
110
115
  })
111
116
 
112
117
  const tagCreateResponseSchema = z.object({
@@ -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 { useRouter, useSearchParams, usePathname } from 'next/navigation'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { Building2, Hash, Users, BarChart3, StickyNote } from 'lucide-react'
8
8
  import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
@@ -16,6 +16,7 @@ import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState,
16
16
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
17
17
  import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
18
18
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
19
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
19
20
  import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
20
21
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
21
22
  import { CollapsibleZoneLayout, type ZoneSectionDescriptor } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
@@ -49,6 +50,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
49
50
  const t = useT()
50
51
  const router = useRouter()
51
52
  const searchParams = useSearchParams()
53
+ const pathname = usePathname()
52
54
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
53
55
 
54
56
  const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
@@ -193,6 +195,21 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
193
195
  logContext: 'customers.companies-v2',
194
196
  })
195
197
 
198
+ // Publish page-load record context to the AppShell-owned `backend:record:current`
199
+ // mount so the enterprise record_locks widget resolves `customers.company` + id
200
+ // explicitly (the hardcoded path allowlist misses the `companies-v2` route).
201
+ useSetCurrentRecordInjectionContext(
202
+ buildRecordInjectionContext({
203
+ resourceKind: 'customers.company',
204
+ resourceId: currentCompanyId,
205
+ updatedAt: (data?.company as { updatedAt?: string | null; updated_at?: string | null } | undefined)?.updatedAt
206
+ ?? (data?.company as { updated_at?: string | null } | undefined)?.updated_at
207
+ ?? null,
208
+ data: data as Record<string, unknown> | null,
209
+ path: pathname,
210
+ }),
211
+ )
212
+
196
213
  const handleEditActivity = React.useCallback((activity: { id: string; interactionType?: string; title?: string | null; body?: string | null; scheduledAt?: string | null; occurredAt?: string | null; [key: string]: unknown }) => {
197
214
  const raw = activity as Record<string, unknown>
198
215
  const durationValue = typeof raw.duration === 'number'
@@ -470,7 +487,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
470
487
  <CrudForm<CompanyEditFormValues>
471
488
  embedded
472
489
  trackDirtyWhenEmbedded
473
- injectionSpotId="customers.company"
490
+ injectionSpotId="crud-form:customers.company"
474
491
  entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
475
492
  schema={formSchema}
476
493
  fields={formFields}
@@ -1,7 +1,9 @@
1
1
  import * as React from 'react'
2
2
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
3
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
3
4
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
4
- import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
5
+ import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
6
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
5
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
8
  import type {
7
9
  CompanyAssociationApiRecord,
@@ -95,6 +97,8 @@ type UseDealAssociationsOptions = {
95
97
  data: DealDetailPayload | null
96
98
  setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>
97
99
  runMutationWithContext: GuardedMutationRunner
100
+ /** Re-fetch the deal detail; wired into the conflict bar's refresh action on a 409. */
101
+ onRefresh?: (() => void) | null
98
102
  }
99
103
 
100
104
  type UseDealAssociationsResult = {
@@ -113,6 +117,7 @@ export function useDealAssociations({
113
117
  data,
114
118
  setData,
115
119
  runMutationWithContext,
120
+ onRefresh,
116
121
  }: UseDealAssociationsOptions): UseDealAssociationsResult {
117
122
  const t = useT()
118
123
  const [peopleEditorIds, setPeopleEditorIds] = React.useState<string[]>([])
@@ -263,7 +268,10 @@ export function useDealAssociations({
263
268
  setPeopleSaving(true)
264
269
  try {
265
270
  await runMutationWithContext(
266
- () => updateCrud('customers/deals', { id: currentDealId, personIds: nextIds }),
271
+ () => withScopedApiRequestHeaders(
272
+ buildOptimisticLockHeader(data?.deal.updatedAt),
273
+ () => updateCrud('customers/deals', { id: currentDealId, personIds: nextIds }),
274
+ ),
267
275
  { id: currentDealId, personIds: nextIds, operation: 'updateDealPeople' },
268
276
  )
269
277
  const nextPeople = await loadPeopleAssociations(nextIds.slice(0, 3))
@@ -277,7 +285,7 @@ export function useDealAssociations({
277
285
  }
278
286
  : prev,
279
287
  )
280
- } catch {
288
+ } catch (error) {
281
289
  setPeopleEditorIds(previousIds)
282
290
  setData((prev) =>
283
291
  prev
@@ -289,12 +297,16 @@ export function useDealAssociations({
289
297
  }
290
298
  : prev,
291
299
  )
292
- flash(t('customers.deals.detail.peopleUpdateError', 'Failed to update linked people.'), 'error')
300
+ // runMutationWithContext already surfaces the conflict bar on a 409; only
301
+ // fall back to the generic flash when this is not a record conflict.
302
+ if (!surfaceRecordConflict(error, t, { onRefresh: onRefresh ?? null })) {
303
+ flash(t('customers.deals.detail.peopleUpdateError', 'Failed to update linked people.'), 'error')
304
+ }
293
305
  } finally {
294
306
  setPeopleSaving(false)
295
307
  }
296
308
  },
297
- [currentDealId, data?.people, loadPeopleAssociations, peopleEditorIds, runMutationWithContext, setData, t],
309
+ [currentDealId, data?.deal.updatedAt, data?.people, loadPeopleAssociations, onRefresh, peopleEditorIds, runMutationWithContext, setData, t],
298
310
  )
299
311
 
300
312
  const handleCompaniesAssociationsChange = React.useCallback(
@@ -307,7 +319,10 @@ export function useDealAssociations({
307
319
  setCompaniesSaving(true)
308
320
  try {
309
321
  await runMutationWithContext(
310
- () => updateCrud('customers/deals', { id: currentDealId, companyIds: nextIds }),
322
+ () => withScopedApiRequestHeaders(
323
+ buildOptimisticLockHeader(data?.deal.updatedAt),
324
+ () => updateCrud('customers/deals', { id: currentDealId, companyIds: nextIds }),
325
+ ),
311
326
  { id: currentDealId, companyIds: nextIds, operation: 'updateDealCompanies' },
312
327
  )
313
328
  const nextCompanies = await loadCompanyAssociations(nextIds.slice(0, 3))
@@ -321,7 +336,7 @@ export function useDealAssociations({
321
336
  }
322
337
  : prev,
323
338
  )
324
- } catch {
339
+ } catch (error) {
325
340
  setCompaniesEditorIds(previousIds)
326
341
  setData((prev) =>
327
342
  prev
@@ -333,7 +348,11 @@ export function useDealAssociations({
333
348
  }
334
349
  : prev,
335
350
  )
336
- flash(t('customers.deals.detail.companiesUpdateError', 'Failed to update linked companies.'), 'error')
351
+ // runMutationWithContext already surfaces the conflict bar on a 409; only
352
+ // fall back to the generic flash when this is not a record conflict.
353
+ if (!surfaceRecordConflict(error, t, { onRefresh: onRefresh ?? null })) {
354
+ flash(t('customers.deals.detail.companiesUpdateError', 'Failed to update linked companies.'), 'error')
355
+ }
337
356
  } finally {
338
357
  setCompaniesSaving(false)
339
358
  }
@@ -342,7 +361,9 @@ export function useDealAssociations({
342
361
  companiesEditorIds,
343
362
  currentDealId,
344
363
  data?.companies,
364
+ data?.deal.updatedAt,
345
365
  loadCompanyAssociations,
366
+ onRefresh,
346
367
  runMutationWithContext,
347
368
  setData,
348
369
  t,
@@ -1,10 +1,15 @@
1
1
  import * as React from 'react'
2
2
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
3
- import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
4
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
5
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
6
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
4
8
  import type { DealStatsPayload, GuardedMutationRunner } from './types'
5
9
 
6
10
  type UseDealClosureOptions = {
7
11
  currentDealId: string | null
12
+ dealUpdatedAt: string | null
8
13
  runMutationWithContext: GuardedMutationRunner
9
14
  confirmDiscardIfDirty: () => Promise<boolean>
10
15
  onClosed: () => Promise<void>
@@ -26,10 +31,12 @@ type UseDealClosureResult = {
26
31
 
27
32
  export function useDealClosure({
28
33
  currentDealId,
34
+ dealUpdatedAt,
29
35
  runMutationWithContext,
30
36
  confirmDiscardIfDirty,
31
37
  onClosed,
32
38
  }: UseDealClosureOptions): UseDealClosureResult {
39
+ const t = useT()
33
40
  const [lostDialogOpen, setLostDialogOpen] = React.useState(false)
34
41
  const [wonPopupOpen, setWonPopupOpen] = React.useState(false)
35
42
  const [lostPopupOpen, setLostPopupOpen] = React.useState(false)
@@ -51,45 +58,65 @@ export function useDealClosure({
51
58
  const handleWon = React.useCallback(async () => {
52
59
  if (!currentDealId) return
53
60
  if (!(await confirmDiscardIfDirty())) return
54
- await runMutationWithContext(
55
- () => updateCrud('customers/deals', { id: currentDealId, closureOutcome: 'won', status: 'win' }),
56
- { id: currentDealId, closureOutcome: 'won', status: 'win', operation: 'closeWon' },
57
- )
61
+ try {
62
+ await runMutationWithContext(
63
+ () => withScopedApiRequestHeaders(
64
+ buildOptimisticLockHeader(dealUpdatedAt),
65
+ () => updateCrud('customers/deals', { id: currentDealId, closureOutcome: 'won', status: 'win' }),
66
+ ),
67
+ { id: currentDealId, closureOutcome: 'won', status: 'win', operation: 'closeWon' },
68
+ )
69
+ } catch (err) {
70
+ if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onClosed() } })) {
71
+ flash(t('customers.deals.detail.closeWonError', 'Failed to mark deal as won.'), 'error')
72
+ }
73
+ return
74
+ }
58
75
  const stats = await fetchDealStats()
59
76
  setWonStats(stats)
60
77
  setWonPopupOpen(true)
61
78
  await onClosed()
62
- }, [confirmDiscardIfDirty, currentDealId, fetchDealStats, onClosed, runMutationWithContext])
79
+ }, [confirmDiscardIfDirty, currentDealId, dealUpdatedAt, fetchDealStats, onClosed, runMutationWithContext, t])
63
80
 
64
81
  const handleLostConfirm = React.useCallback(
65
82
  async (input: { lossReasonId: string; lossNotes?: string }) => {
66
83
  if (!currentDealId) return
67
84
  if (!(await confirmDiscardIfDirty())) return
68
- await runMutationWithContext(
69
- () =>
70
- updateCrud('customers/deals', {
85
+ try {
86
+ await runMutationWithContext(
87
+ () => withScopedApiRequestHeaders(
88
+ buildOptimisticLockHeader(dealUpdatedAt),
89
+ () =>
90
+ updateCrud('customers/deals', {
91
+ id: currentDealId,
92
+ closureOutcome: 'lost',
93
+ status: 'loose',
94
+ lossReasonId: input.lossReasonId,
95
+ lossNotes: input.lossNotes ?? null,
96
+ }),
97
+ ),
98
+ {
71
99
  id: currentDealId,
72
100
  closureOutcome: 'lost',
73
101
  status: 'loose',
74
102
  lossReasonId: input.lossReasonId,
75
103
  lossNotes: input.lossNotes ?? null,
76
- }),
77
- {
78
- id: currentDealId,
79
- closureOutcome: 'lost',
80
- status: 'loose',
81
- lossReasonId: input.lossReasonId,
82
- lossNotes: input.lossNotes ?? null,
83
- operation: 'closeLost',
84
- },
85
- )
104
+ operation: 'closeLost',
105
+ },
106
+ )
107
+ } catch (err) {
108
+ if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onClosed() } })) {
109
+ flash(t('customers.deals.detail.closeLostError', 'Failed to mark deal as lost.'), 'error')
110
+ }
111
+ return
112
+ }
86
113
  setLostDialogOpen(false)
87
114
  const stats = await fetchDealStats()
88
115
  setLostStats(stats)
89
116
  setLostPopupOpen(true)
90
117
  await onClosed()
91
118
  },
92
- [confirmDiscardIfDirty, currentDealId, fetchDealStats, onClosed, runMutationWithContext],
119
+ [confirmDiscardIfDirty, currentDealId, dealUpdatedAt, fetchDealStats, onClosed, runMutationWithContext, t],
93
120
  )
94
121
 
95
122
  const openLostDialog = React.useCallback(() => setLostDialogOpen(true), [])
@@ -1,6 +1,9 @@
1
1
  import * as React from 'react'
2
2
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
3
3
  import { updateCrud } from '@open-mercato/ui/backend/utils/crud'
4
+ import { withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
5
+ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
6
+ import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
4
7
  import { useT } from '@open-mercato/shared/lib/i18n/context'
5
8
  import type { DealDetailPayload, GuardedMutationRunner } from './types'
6
9
 
@@ -35,13 +38,20 @@ export function useDealPipeline({
35
38
  setIsStageSaving(true)
36
39
  try {
37
40
  await runMutationWithContext(
38
- () => updateCrud('customers/deals', { id: currentDealId, pipelineStageId: nextStageId }),
41
+ () => withScopedApiRequestHeaders(
42
+ buildOptimisticLockHeader(data.deal.updatedAt),
43
+ () => updateCrud('customers/deals', { id: currentDealId, pipelineStageId: nextStageId }),
44
+ ),
39
45
  { id: currentDealId, pipelineStageId: nextStageId, operation: 'updateDealStage' },
40
46
  )
41
47
  flash(t('customers.deals.detail.stageUpdateSuccess', 'Deal stage updated.'), 'success')
42
48
  await onStageChanged()
43
- } catch {
44
- flash(t('customers.deals.detail.stageUpdateError', 'Failed to update deal stage.'), 'error')
49
+ } catch (err) {
50
+ // A concurrent edit / pessimistic lock 409 routes to the unified conflict
51
+ // bar or merge dialog (S3); surface any other error as a flash.
52
+ if (!surfaceRecordConflict(err, t, { onRefresh: () => { void onStageChanged() } })) {
53
+ flash(t('customers.deals.detail.stageUpdateError', 'Failed to update deal stage.'), 'error')
54
+ }
45
55
  } finally {
46
56
  setIsStageSaving(false)
47
57
  }
@@ -4,11 +4,12 @@ import * as React from 'react'
4
4
  import Link from 'next/link'
5
5
  import { Building2, UserSearch, Users } from 'lucide-react'
6
6
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
7
- import { useRouter, useSearchParams } from 'next/navigation'
7
+ import { useRouter, useSearchParams, usePathname } from 'next/navigation'
8
8
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
10
  import { AttachmentsSection, ErrorMessage, LoadingMessage, NotesSection, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
11
11
  import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
12
+ import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
12
13
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
13
14
  import { CollapsibleZoneLayout } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
14
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
@@ -52,6 +53,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
52
53
  const t = useT()
53
54
  const router = useRouter()
54
55
  const searchParams = useSearchParams()
56
+ const pathname = usePathname()
55
57
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
56
58
  const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
57
59
 
@@ -80,6 +82,20 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
80
82
  data,
81
83
  })
82
84
 
85
+ // Publish page-load record context with explicit `updatedAt`/`data` to the
86
+ // AppShell-owned `backend:record:current` mount. The deal path is already in the
87
+ // widget's allowlist (presence works) but supplied no version/record payload —
88
+ // this gives the merge dialog its action-log base and field-diff source.
89
+ useSetCurrentRecordInjectionContext(
90
+ buildRecordInjectionContext({
91
+ resourceKind: 'customers.deal',
92
+ resourceId: data?.deal.id ?? null,
93
+ updatedAt: data?.deal.updatedAt ?? null,
94
+ data: data as Record<string, unknown> | null,
95
+ path: pathname,
96
+ }),
97
+ )
98
+
83
99
  const notesAdapter = React.useMemo(
84
100
  () => createCustomerNotesAdapter(detailTranslator, { runMutation: runMutationWithContext }),
85
101
  [detailTranslator, runMutationWithContext],
@@ -171,6 +187,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
171
187
  data,
172
188
  setData,
173
189
  runMutationWithContext,
190
+ onRefresh: () => { void loadData() },
174
191
  })
175
192
 
176
193
  const { isStageSaving, handleStageChange } = useDealPipeline({
@@ -195,6 +212,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
195
212
  handleLostConfirm,
196
213
  } = useDealClosure({
197
214
  currentDealId,
215
+ dealUpdatedAt: data?.deal.updatedAt ?? null,
198
216
  runMutationWithContext,
199
217
  confirmDiscardIfDirty,
200
218
  onClosed: loadData,
@@ -365,6 +383,8 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
365
383
  showAssociationsGroup={false}
366
384
  showVersionHistory={false}
367
385
  showCancelAction={false}
386
+ injectionSpotId="crud-form:customers.deal"
387
+ optimisticLockUpdatedAt={data.deal.updatedAt}
368
388
  onDirtyChange={setIsDirty}
369
389
  initialPipelineOptions={formPipelineOptions}
370
390
  initialPipelineStageOptions={data.pipelineStages}