@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6227.1.2695efff30

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 (198) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/currencies/backend/currencies/[id]/page.js +33 -8
  3. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  4. package/dist/modules/currencies/backend/currencies/page.js +63 -27
  5. package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
  6. package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
  7. package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
  8. package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
  9. package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
  10. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
  11. package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
  12. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
  13. package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
  14. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
  15. package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
  16. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
  17. package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
  18. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
  19. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
  20. package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
  21. package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
  22. package/dist/modules/customers/components/DictionarySettings.js +57 -31
  23. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  24. package/dist/modules/customers/components/PipelineSettings.js +156 -76
  25. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  26. package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
  27. package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
  28. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
  29. package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
  30. package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
  31. package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
  32. package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
  33. package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
  34. package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
  35. package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
  36. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
  37. package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
  38. package/dist/modules/customers/lib/calendar/labels.js +33 -0
  39. package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
  40. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
  41. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  42. package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
  43. package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
  44. package/dist/modules/data_sync/api/mappings/route.js +44 -0
  45. package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
  46. package/dist/modules/data_sync/api/run.js +31 -0
  47. package/dist/modules/data_sync/api/run.js.map +2 -2
  48. package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
  49. package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
  50. package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
  51. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  52. package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
  53. package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
  54. package/dist/modules/data_sync/api/schedules/route.js +33 -4
  55. package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
  56. package/dist/modules/directory/api/organization-branding/route.js +54 -2
  57. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  58. package/dist/modules/directory/backend/directory/branding/page.js +15 -9
  59. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  60. package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
  61. package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
  62. package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
  63. package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
  64. package/dist/modules/entities/api/definitions.batch.js +15 -0
  65. package/dist/modules/entities/api/definitions.batch.js.map +2 -2
  66. package/dist/modules/entities/api/definitions.js +26 -0
  67. package/dist/modules/entities/api/definitions.js.map +2 -2
  68. package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
  69. package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
  70. package/dist/modules/entities/api/definitions.restore.js +15 -0
  71. package/dist/modules/entities/api/definitions.restore.js.map +2 -2
  72. package/dist/modules/entities/api/entities.js +31 -3
  73. package/dist/modules/entities/api/entities.js.map +2 -2
  74. package/dist/modules/entities/api/records.js +18 -0
  75. package/dist/modules/entities/api/records.js.map +2 -2
  76. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
  77. package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
  78. package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
  79. package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
  80. package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
  81. package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
  82. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
  83. package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
  84. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
  85. package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
  86. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
  87. package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
  88. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
  89. package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
  90. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
  91. package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
  92. package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
  93. package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
  94. package/dist/modules/payment_gateways/api/capture/route.js +37 -0
  95. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  96. package/dist/modules/payment_gateways/api/guards.js +31 -0
  97. package/dist/modules/payment_gateways/api/guards.js.map +7 -0
  98. package/dist/modules/payment_gateways/api/refund/route.js +37 -0
  99. package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
  100. package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
  101. package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
  102. package/dist/modules/planner/api/availability-date-specific.js +11 -1
  103. package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
  104. package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
  105. package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
  106. package/dist/modules/planner/commands/availability-date-specific.js +16 -0
  107. package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
  108. package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
  109. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  110. package/dist/modules/query_index/api/purge.js +35 -3
  111. package/dist/modules/query_index/api/purge.js.map +2 -2
  112. package/dist/modules/query_index/api/reindex.js +41 -3
  113. package/dist/modules/query_index/api/reindex.js.map +2 -2
  114. package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
  115. package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
  116. package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
  117. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  118. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
  119. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
  120. package/dist/modules/shipping_carriers/data/validators.js +6 -1
  121. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  122. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
  123. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
  124. package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
  125. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  126. package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
  127. package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
  128. package/dist/modules/translations/components/TranslationManager.js +49 -20
  129. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  130. package/package.json +7 -7
  131. package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
  132. package/src/modules/currencies/backend/currencies/page.tsx +68 -29
  133. package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
  134. package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
  135. package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
  136. package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
  137. package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
  138. package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
  139. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
  140. package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
  141. package/src/modules/customers/components/DictionarySettings.tsx +63 -29
  142. package/src/modules/customers/components/PipelineSettings.tsx +165 -80
  143. package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
  144. package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
  145. package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
  146. package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
  147. package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
  148. package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
  149. package/src/modules/customers/i18n/de.json +11 -7
  150. package/src/modules/customers/i18n/en.json +6 -2
  151. package/src/modules/customers/i18n/es.json +11 -7
  152. package/src/modules/customers/i18n/pl.json +11 -7
  153. package/src/modules/customers/lib/calendar/labels.ts +42 -0
  154. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
  155. package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
  156. package/src/modules/data_sync/api/mappings/route.ts +48 -0
  157. package/src/modules/data_sync/api/run.ts +33 -0
  158. package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
  159. package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
  160. package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
  161. package/src/modules/data_sync/api/schedules/route.ts +36 -4
  162. package/src/modules/directory/api/organization-branding/route.ts +61 -0
  163. package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
  164. package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
  165. package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
  166. package/src/modules/entities/api/definitions.batch.ts +17 -0
  167. package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
  168. package/src/modules/entities/api/definitions.restore.ts +17 -0
  169. package/src/modules/entities/api/definitions.ts +30 -0
  170. package/src/modules/entities/api/entities.ts +35 -3
  171. package/src/modules/entities/api/records.ts +20 -0
  172. package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
  173. package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
  174. package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
  175. package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
  176. package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
  177. package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
  178. package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
  179. package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
  180. package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
  181. package/src/modules/payment_gateways/api/capture/route.ts +39 -0
  182. package/src/modules/payment_gateways/api/guards.ts +59 -0
  183. package/src/modules/payment_gateways/api/refund/route.ts +39 -0
  184. package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
  185. package/src/modules/planner/api/availability-date-specific.ts +10 -0
  186. package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
  187. package/src/modules/planner/commands/availability-date-specific.ts +24 -0
  188. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
  189. package/src/modules/query_index/api/purge.ts +37 -3
  190. package/src/modules/query_index/api/reindex.ts +43 -3
  191. package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
  192. package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
  193. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
  194. package/src/modules/shipping_carriers/data/validators.ts +5 -0
  195. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
  196. package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
  197. package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
  198. package/src/modules/translations/components/TranslationManager.tsx +65 -21
@@ -5,6 +5,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
5
5
  import { useT } from '@open-mercato/shared/lib/i18n/context'
6
6
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
7
7
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
8
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
8
9
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
9
10
  import { Button } from '@open-mercato/ui/primitives/button'
10
11
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -66,6 +67,31 @@ export default function PipelineStagesPage() {
66
67
  const [stageIcon, setStageIcon] = React.useState<string | null>(null)
67
68
  const [saving, setSaving] = React.useState(false)
68
69
 
70
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
71
+ formId: string
72
+ resourceKind: string
73
+ retryLastMutation: () => Promise<boolean>
74
+ }>({
75
+ contextId: 'customers-pipeline-stages-page',
76
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
77
+ })
78
+ const pipelineMutationContext = React.useMemo(
79
+ () => ({
80
+ formId: 'customers-pipeline-stages-page',
81
+ resourceKind: 'customers.pipeline',
82
+ retryLastMutation,
83
+ }),
84
+ [retryLastMutation],
85
+ )
86
+ const stageMutationContext = React.useMemo(
87
+ () => ({
88
+ formId: 'customers-pipeline-stages-page',
89
+ resourceKind: 'customers.pipeline_stage',
90
+ retryLastMutation,
91
+ }),
92
+ [retryLastMutation],
93
+ )
94
+
69
95
  const selectedPipeline = React.useMemo(
70
96
  () => pipelines.find((p) => p.id === selectedPipelineId) ?? null,
71
97
  [pipelines, selectedPipelineId],
@@ -135,30 +161,54 @@ export default function PipelineStagesPage() {
135
161
  setSaving(true)
136
162
  try {
137
163
  if (pipelineDialog?.mode === 'create') {
138
- const result = await apiCall<{ id: string }>('/api/customers/pipelines', {
139
- method: 'POST',
140
- body: JSON.stringify({ name: pipelineName.trim(), isDefault: pipelineIsDefault }),
141
- headers: { 'Content-Type': 'application/json' },
142
- })
143
- if (!result.ok) {
144
- flash(t('customers.config.pipelineStages.errorCreatePipeline', 'Failed to create pipeline'), 'error')
164
+ let newId: string | null = null
165
+ try {
166
+ await runMutation({
167
+ operation: async () => {
168
+ const result = await apiCall<{ id: string }>('/api/customers/pipelines', {
169
+ method: 'POST',
170
+ body: JSON.stringify({ name: pipelineName.trim(), isDefault: pipelineIsDefault }),
171
+ headers: { 'Content-Type': 'application/json' },
172
+ })
173
+ if (!result.ok) {
174
+ throw new Error(t('customers.config.pipelineStages.errorCreatePipeline', 'Failed to create pipeline'))
175
+ }
176
+ newId = result.result?.id ?? null
177
+ },
178
+ context: pipelineMutationContext,
179
+ mutationPayload: { action: 'create', name: pipelineName.trim(), isDefault: pipelineIsDefault },
180
+ })
181
+ } catch (err) {
182
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorCreatePipeline', 'Failed to create pipeline')
183
+ flash(msg, 'error')
145
184
  return
146
185
  }
147
186
  flash(t('customers.config.pipelineStages.createdPipeline', 'Pipeline created'), 'success')
148
- const newId = result.result?.id ?? null
149
187
  await loadPipelines()
150
188
  if (newId) setSelectedPipelineId(newId)
151
189
  } else if (pipelineDialog?.mode === 'edit') {
152
- const result = await withScopedApiRequestHeaders(
153
- buildOptimisticLockHeader(pipelineDialog.pipeline.updatedAt),
154
- () => apiCall('/api/customers/pipelines', {
155
- method: 'PUT',
156
- body: JSON.stringify({ id: pipelineDialog.pipeline.id, name: pipelineName.trim(), isDefault: pipelineIsDefault }),
157
- headers: { 'Content-Type': 'application/json' },
158
- }),
159
- )
160
- if (!result.ok) {
161
- flash(t('customers.config.pipelineStages.errorUpdatePipeline', 'Failed to update pipeline'), 'error')
190
+ const targetPipeline = pipelineDialog.pipeline
191
+ try {
192
+ await runMutation({
193
+ operation: async () => {
194
+ const result = await withScopedApiRequestHeaders(
195
+ buildOptimisticLockHeader(targetPipeline.updatedAt),
196
+ () => apiCall('/api/customers/pipelines', {
197
+ method: 'PUT',
198
+ body: JSON.stringify({ id: targetPipeline.id, name: pipelineName.trim(), isDefault: pipelineIsDefault }),
199
+ headers: { 'Content-Type': 'application/json' },
200
+ }),
201
+ )
202
+ if (!result.ok) {
203
+ throw new Error(t('customers.config.pipelineStages.errorUpdatePipeline', 'Failed to update pipeline'))
204
+ }
205
+ },
206
+ context: pipelineMutationContext,
207
+ mutationPayload: { action: 'update', id: targetPipeline.id, name: pipelineName.trim(), isDefault: pipelineIsDefault },
208
+ })
209
+ } catch (err) {
210
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorUpdatePipeline', 'Failed to update pipeline')
211
+ flash(msg, 'error')
162
212
  return
163
213
  }
164
214
  flash(t('customers.config.pipelineStages.updatedPipeline', 'Pipeline updated'), 'success')
@@ -181,17 +231,28 @@ export default function PipelineStagesPage() {
181
231
  variant: 'destructive',
182
232
  })
183
233
  if (!confirmed) return
184
- const result = await withScopedApiRequestHeaders(
185
- buildOptimisticLockHeader(pipeline.updatedAt),
186
- () => apiCall('/api/customers/pipelines', {
187
- method: 'DELETE',
188
- body: JSON.stringify({ id: pipeline.id }),
189
- headers: { 'Content-Type': 'application/json' },
190
- }),
191
- )
192
- if (!result.ok) {
193
- const error = (result.result as { error?: string })?.error
194
- flash(error ?? t('customers.config.pipelineStages.errorDeletePipeline', 'Failed to delete pipeline'), 'error')
234
+ try {
235
+ await runMutation({
236
+ operation: async () => {
237
+ const result = await withScopedApiRequestHeaders(
238
+ buildOptimisticLockHeader(pipeline.updatedAt),
239
+ () => apiCall('/api/customers/pipelines', {
240
+ method: 'DELETE',
241
+ body: JSON.stringify({ id: pipeline.id }),
242
+ headers: { 'Content-Type': 'application/json' },
243
+ }),
244
+ )
245
+ if (!result.ok) {
246
+ const error = (result.result as { error?: string })?.error
247
+ throw new Error(error ?? t('customers.config.pipelineStages.errorDeletePipeline', 'Failed to delete pipeline'))
248
+ }
249
+ },
250
+ context: pipelineMutationContext,
251
+ mutationPayload: { action: 'delete', id: pipeline.id },
252
+ })
253
+ } catch (err) {
254
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorDeletePipeline', 'Failed to delete pipeline')
255
+ flash(msg, 'error')
195
256
  return
196
257
  }
197
258
  flash(t('customers.config.pipelineStages.deletedPipeline', 'Pipeline deleted'), 'success')
@@ -222,27 +283,50 @@ export default function PipelineStagesPage() {
222
283
  ...(stageColor !== null ? { color: stageColor } : {}),
223
284
  ...(stageIcon !== null ? { icon: stageIcon } : {}),
224
285
  }
225
- const result = await apiCall('/api/customers/pipeline-stages', {
226
- method: 'POST',
227
- body: JSON.stringify({ pipelineId: selectedPipelineId, label: stageName.trim(), ...appearancePayload }),
228
- headers: { 'Content-Type': 'application/json' },
229
- })
230
- if (!result.ok) {
231
- flash(t('customers.config.pipelineStages.errorCreateStage', 'Failed to create stage'), 'error')
286
+ try {
287
+ await runMutation({
288
+ operation: async () => {
289
+ const result = await apiCall('/api/customers/pipeline-stages', {
290
+ method: 'POST',
291
+ body: JSON.stringify({ pipelineId: selectedPipelineId, label: stageName.trim(), ...appearancePayload }),
292
+ headers: { 'Content-Type': 'application/json' },
293
+ })
294
+ if (!result.ok) {
295
+ throw new Error(t('customers.config.pipelineStages.errorCreateStage', 'Failed to create stage'))
296
+ }
297
+ },
298
+ context: stageMutationContext,
299
+ mutationPayload: { action: 'create', pipelineId: selectedPipelineId, label: stageName.trim(), ...appearancePayload },
300
+ })
301
+ } catch (err) {
302
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorCreateStage', 'Failed to create stage')
303
+ flash(msg, 'error')
232
304
  return
233
305
  }
234
306
  flash(t('customers.config.pipelineStages.createdStage', 'Stage created'), 'success')
235
307
  } else if (stageDialog?.mode === 'edit') {
236
- const result = await withScopedApiRequestHeaders(
237
- buildOptimisticLockHeader(stageDialog.stage.updatedAt),
238
- () => apiCall('/api/customers/pipeline-stages', {
239
- method: 'PUT',
240
- body: JSON.stringify({ id: stageDialog.stage.id, label: stageName.trim(), color: stageColor, icon: stageIcon }),
241
- headers: { 'Content-Type': 'application/json' },
242
- }),
243
- )
244
- if (!result.ok) {
245
- flash(t('customers.config.pipelineStages.errorUpdateStage', 'Failed to update stage'), 'error')
308
+ const targetStage = stageDialog.stage
309
+ try {
310
+ await runMutation({
311
+ operation: async () => {
312
+ const result = await withScopedApiRequestHeaders(
313
+ buildOptimisticLockHeader(targetStage.updatedAt),
314
+ () => apiCall('/api/customers/pipeline-stages', {
315
+ method: 'PUT',
316
+ body: JSON.stringify({ id: targetStage.id, label: stageName.trim(), color: stageColor, icon: stageIcon }),
317
+ headers: { 'Content-Type': 'application/json' },
318
+ }),
319
+ )
320
+ if (!result.ok) {
321
+ throw new Error(t('customers.config.pipelineStages.errorUpdateStage', 'Failed to update stage'))
322
+ }
323
+ },
324
+ context: stageMutationContext,
325
+ mutationPayload: { action: 'update', id: targetStage.id, label: stageName.trim(), color: stageColor, icon: stageIcon },
326
+ })
327
+ } catch (err) {
328
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorUpdateStage', 'Failed to update stage')
329
+ flash(msg, 'error')
246
330
  return
247
331
  }
248
332
  flash(t('customers.config.pipelineStages.updatedStage', 'Stage updated'), 'success')
@@ -265,17 +349,28 @@ export default function PipelineStagesPage() {
265
349
  variant: 'destructive',
266
350
  })
267
351
  if (!confirmed) return
268
- const result = await withScopedApiRequestHeaders(
269
- buildOptimisticLockHeader(stage.updatedAt),
270
- () => apiCall('/api/customers/pipeline-stages', {
271
- method: 'DELETE',
272
- body: JSON.stringify({ id: stage.id }),
273
- headers: { 'Content-Type': 'application/json' },
274
- }),
275
- )
276
- if (!result.ok) {
277
- const error = (result.result as { error?: string })?.error
278
- flash(error ?? t('customers.config.pipelineStages.errorDeleteStage', 'Failed to delete stage'), 'error')
352
+ try {
353
+ await runMutation({
354
+ operation: async () => {
355
+ const result = await withScopedApiRequestHeaders(
356
+ buildOptimisticLockHeader(stage.updatedAt),
357
+ () => apiCall('/api/customers/pipeline-stages', {
358
+ method: 'DELETE',
359
+ body: JSON.stringify({ id: stage.id }),
360
+ headers: { 'Content-Type': 'application/json' },
361
+ }),
362
+ )
363
+ if (!result.ok) {
364
+ const error = (result.result as { error?: string })?.error
365
+ throw new Error(error ?? t('customers.config.pipelineStages.errorDeleteStage', 'Failed to delete stage'))
366
+ }
367
+ },
368
+ context: stageMutationContext,
369
+ mutationPayload: { action: 'delete', id: stage.id },
370
+ })
371
+ } catch (err) {
372
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorDeleteStage', 'Failed to delete stage')
373
+ flash(msg, 'error')
279
374
  return
280
375
  }
281
376
  flash(t('customers.config.pipelineStages.deletedStage', 'Stage deleted'), 'success')
@@ -293,13 +388,25 @@ export default function PipelineStagesPage() {
293
388
  const updated = reordered.map((stage, i) => ({ ...stage, order: i }))
294
389
  setStages(updated)
295
390
 
296
- const result = await apiCall('/api/customers/pipeline-stages/reorder', {
297
- method: 'POST',
298
- body: JSON.stringify({ stages: updated.map((s) => ({ id: s.id, order: s.order })) }),
299
- headers: { 'Content-Type': 'application/json' },
300
- })
301
- if (!result.ok) {
302
- flash(t('customers.config.pipelineStages.errorReorder', 'Failed to reorder stages'), 'error')
391
+ const orderedStages = updated.map((s) => ({ id: s.id, order: s.order }))
392
+ try {
393
+ await runMutation({
394
+ operation: async () => {
395
+ const result = await apiCall('/api/customers/pipeline-stages/reorder', {
396
+ method: 'POST',
397
+ body: JSON.stringify({ stages: orderedStages }),
398
+ headers: { 'Content-Type': 'application/json' },
399
+ })
400
+ if (!result.ok) {
401
+ throw new Error(t('customers.config.pipelineStages.errorReorder', 'Failed to reorder stages'))
402
+ }
403
+ },
404
+ context: stageMutationContext,
405
+ mutationPayload: { action: 'reorder', stages: orderedStages },
406
+ })
407
+ } catch (err) {
408
+ const msg = err instanceof Error && err.message ? err.message : t('customers.config.pipelineStages.errorReorder', 'Failed to reorder stages')
409
+ flash(msg, 'error')
303
410
  if (selectedPipelineId) await loadStages(selectedPipelineId)
304
411
  }
305
412
  }
@@ -5,9 +5,12 @@ import { Spinner } from '@open-mercato/ui/primitives/spinner'
5
5
  import { RadioGroup, Radio } from '@open-mercato/ui/primitives/radio'
6
6
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
7
7
  import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
8
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
8
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
9
10
  import type { AddressFormatStrategy } from '../utils/addressFormat'
10
11
 
12
+ const SAVE_CONTEXT_ID = 'customers-address-format-settings'
13
+
11
14
  type Option = {
12
15
  id: AddressFormatStrategy
13
16
  title: string
@@ -21,6 +24,15 @@ export function AddressFormatSettings() {
21
24
  const [pending, setPending] = React.useState<AddressFormatStrategy | null>(null)
22
25
  const [error, setError] = React.useState<string | null>(null)
23
26
 
27
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
28
+ formId: string
29
+ resourceKind: string
30
+ retryLastMutation: () => Promise<boolean>
31
+ }>({
32
+ contextId: SAVE_CONTEXT_ID,
33
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
34
+ })
35
+
24
36
  const options = React.useMemo<Option[]>(
25
37
  () => [
26
38
  {
@@ -87,25 +99,33 @@ export function AddressFormatSettings() {
87
99
  setPending(next)
88
100
  setError(null)
89
101
  try {
90
- // optimistic-lock-exempt: single-row tenant address-format preference toggle — no per-record version / concurrent record edit
91
- const call = await apiCall<Record<string, unknown>>(
92
- '/api/customers/settings/address-format',
93
- {
94
- method: 'PUT',
95
- headers: { 'content-type': 'application/json' },
96
- body: JSON.stringify({ addressFormat: next }),
102
+ await runMutation({
103
+ // optimistic-lock-exempt: single-row tenant address-format preference toggle — no per-record version / concurrent record edit
104
+ operation: async () => {
105
+ const call = await apiCall<Record<string, unknown>>(
106
+ '/api/customers/settings/address-format',
107
+ {
108
+ method: 'PUT',
109
+ headers: { 'content-type': 'application/json' },
110
+ body: JSON.stringify({ addressFormat: next }),
111
+ },
112
+ )
113
+ const payload = call.result ?? {}
114
+ if (!call.ok) {
115
+ const message =
116
+ typeof payload?.error === 'string'
117
+ ? payload.error
118
+ : t('customers.config.addressFormat.errorSave', 'Failed to update address settings')
119
+ throw new Error(message)
120
+ }
97
121
  },
98
- )
99
- const payload = call.result ?? {}
100
- if (!call.ok) {
101
- const message =
102
- typeof payload?.error === 'string'
103
- ? payload.error
104
- : t('customers.config.addressFormat.errorSave', 'Failed to update address settings')
105
- setError(message)
106
- flash(message, 'error')
107
- return
108
- }
122
+ context: {
123
+ formId: SAVE_CONTEXT_ID,
124
+ resourceKind: 'customers.settings',
125
+ retryLastMutation,
126
+ },
127
+ mutationPayload: { addressFormat: next },
128
+ })
109
129
  setFormat(next)
110
130
  flash(t('customers.config.addressFormat.success', 'Address format updated'), 'success')
111
131
  } catch (err) {
@@ -119,7 +139,7 @@ export function AddressFormatSettings() {
119
139
  setPending(null)
120
140
  }
121
141
  },
122
- [format, t]
142
+ [format, retryLastMutation, runMutation, t]
123
143
  )
124
144
 
125
145
  return (
@@ -10,6 +10,7 @@ import {
10
10
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
11
  import { apiCallOrThrow, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
13
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
13
14
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
14
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
16
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
@@ -128,6 +129,24 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
128
129
  const [dialog, setDialog] = React.useState<DialogState | null>(null)
129
130
  const [submitting, setSubmitting] = React.useState(false)
130
131
 
132
+ const saveContextId = `customers-dictionary-${kind}`
133
+ const { runMutation, retryLastMutation } = useGuardedMutation<{
134
+ formId: string
135
+ resourceKind: string
136
+ retryLastMutation: () => Promise<boolean>
137
+ }>({
138
+ contextId: saveContextId,
139
+ blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
140
+ })
141
+ const mutationContext = React.useMemo(
142
+ () => ({
143
+ formId: saveContextId,
144
+ resourceKind: 'customers.dictionary',
145
+ retryLastMutation,
146
+ }),
147
+ [retryLastMutation, saveContextId],
148
+ )
149
+
131
150
  const inheritedActionBlocked = t('customers.config.dictionaries.inherited.blocked', 'Inherited entries can only be edited from the parent organization.')
132
151
  const inheritedTooltip = t('customers.config.dictionaries.inherited.tooltip', 'Managed in parent organization')
133
152
  const inheritedLabel = t('customers.config.dictionaries.inherited.label', 'Inherited')
@@ -221,13 +240,18 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
221
240
  })
222
241
  if (!confirmed) return
223
242
  try {
224
- await withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () =>
225
- apiCallOrThrow(
226
- `/api/customers/dictionaries/${kind}/${encodeURIComponent(entry.id)}`,
227
- { method: 'DELETE' },
228
- { errorMessage: errorDelete },
229
- ),
230
- )
243
+ await runMutation({
244
+ operation: () =>
245
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () =>
246
+ apiCallOrThrow(
247
+ `/api/customers/dictionaries/${kind}/${encodeURIComponent(entry.id)}`,
248
+ { method: 'DELETE' },
249
+ { errorMessage: errorDelete },
250
+ ),
251
+ ),
252
+ context: mutationContext,
253
+ mutationPayload: { action: 'delete', id: entry.id, kind },
254
+ })
231
255
  flash(successDelete, 'success')
232
256
  await loadEntries()
233
257
  } catch (err) {
@@ -245,7 +269,7 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
245
269
  const messageValue = err instanceof Error ? err.message : errorDelete
246
270
  flash(messageValue, 'error')
247
271
  }
248
- }, [confirm, deleteConfirmTemplate, errorDelete, formatCountMessage, inheritedActionBlocked, kind, loadEntries, roleTypeDeleteBlockedText, roleTypeDeleteBlockedTitle, successDelete, t])
272
+ }, [confirm, deleteConfirmTemplate, errorDelete, formatCountMessage, inheritedActionBlocked, kind, loadEntries, mutationContext, roleTypeDeleteBlockedText, roleTypeDeleteBlockedTitle, runMutation, successDelete, t])
249
273
 
250
274
  const submitForm = React.useCallback(async (values: DictionaryFormValues) => {
251
275
  const payload = {
@@ -257,15 +281,20 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
257
281
  setSubmitting(true)
258
282
  try {
259
283
  if (!dialog || dialog.mode === 'create') {
260
- await apiCallOrThrow(
261
- `/api/customers/dictionaries/${kind}`,
262
- {
263
- method: 'POST',
264
- headers: { 'content-type': 'application/json' },
265
- body: JSON.stringify(payload),
266
- },
267
- { errorMessage: errorSave },
268
- )
284
+ await runMutation({
285
+ operation: () =>
286
+ apiCallOrThrow(
287
+ `/api/customers/dictionaries/${kind}`,
288
+ {
289
+ method: 'POST',
290
+ headers: { 'content-type': 'application/json' },
291
+ body: JSON.stringify(payload),
292
+ },
293
+ { errorMessage: errorSave },
294
+ ),
295
+ context: mutationContext,
296
+ mutationPayload: { action: 'create', kind, ...payload },
297
+ })
269
298
  flash(successSave, 'success')
270
299
  } else if (dialog.mode === 'edit') {
271
300
  const target = dialog.entry
@@ -284,17 +313,22 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
284
313
  closeDialog()
285
314
  return
286
315
  }
287
- await withScopedApiRequestHeaders(buildOptimisticLockHeader(target.updatedAt), () =>
288
- apiCallOrThrow(
289
- `/api/customers/dictionaries/${kind}/${encodeURIComponent(target.id)}`,
290
- {
291
- method: 'PATCH',
292
- headers: { 'content-type': 'application/json' },
293
- body: JSON.stringify(body),
294
- },
295
- { errorMessage: errorSave },
296
- ),
297
- )
316
+ await runMutation({
317
+ operation: () =>
318
+ withScopedApiRequestHeaders(buildOptimisticLockHeader(target.updatedAt), () =>
319
+ apiCallOrThrow(
320
+ `/api/customers/dictionaries/${kind}/${encodeURIComponent(target.id)}`,
321
+ {
322
+ method: 'PATCH',
323
+ headers: { 'content-type': 'application/json' },
324
+ body: JSON.stringify(body),
325
+ },
326
+ { errorMessage: errorSave },
327
+ ),
328
+ ),
329
+ context: mutationContext,
330
+ mutationPayload: { action: 'update', id: target.id, kind, ...body },
331
+ })
298
332
  flash(successSave, 'success')
299
333
  }
300
334
  closeDialog()
@@ -305,7 +339,7 @@ function CustomerDictionarySection({ kind, title, description }: CustomerDiction
305
339
  } finally {
306
340
  setSubmitting(false)
307
341
  }
308
- }, [closeDialog, dialog, errorSave, inheritedActionBlocked, kind, loadEntries, successSave])
342
+ }, [closeDialog, dialog, errorSave, inheritedActionBlocked, kind, loadEntries, mutationContext, runMutation, successSave])
309
343
 
310
344
  const currentValues = React.useMemo<DictionaryFormValues>(() => {
311
345
  if (dialog && dialog.mode === 'edit') {