@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6241.1.47c01ad4ed
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -290,19 +290,35 @@ export default function PaymentTransactionsPage() {
|
|
|
290
290
|
const handleRefreshStatus = React.useCallback(async () => {
|
|
291
291
|
if (!selectedId) return
|
|
292
292
|
setIsRefreshingStatus(true)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
try {
|
|
294
|
+
await runMutation({
|
|
295
|
+
operation: async () => {
|
|
296
|
+
await apiCallOrThrow('/api/payment_gateways/status', {
|
|
297
|
+
method: 'POST',
|
|
298
|
+
headers: { 'content-type': 'application/json' },
|
|
299
|
+
body: JSON.stringify({ transactionId: selectedId }),
|
|
300
|
+
})
|
|
301
|
+
},
|
|
302
|
+
context: {
|
|
303
|
+
entityType: 'payment_gateways:gateway_transaction',
|
|
304
|
+
entityId: selectedId,
|
|
305
|
+
},
|
|
306
|
+
mutationPayload: { transactionId: selectedId },
|
|
307
|
+
})
|
|
308
|
+
await Promise.all([
|
|
309
|
+
loadRows(),
|
|
310
|
+
loadDetail(selectedId),
|
|
311
|
+
])
|
|
312
|
+
flash(t('payment_gateways.transactions.success.refreshStatus', 'Transaction status refreshed'), 'success')
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const message = error instanceof Error && error.message
|
|
315
|
+
? error.message
|
|
316
|
+
: t('payment_gateways.transactions.error.refreshStatus', 'Failed to refresh transaction status')
|
|
317
|
+
flash(message, 'error')
|
|
318
|
+
} finally {
|
|
296
319
|
setIsRefreshingStatus(false)
|
|
297
|
-
return
|
|
298
320
|
}
|
|
299
|
-
|
|
300
|
-
loadRows(),
|
|
301
|
-
loadDetail(selectedId),
|
|
302
|
-
])
|
|
303
|
-
flash(t('payment_gateways.transactions.success.refreshStatus', 'Transaction status refreshed'), 'success')
|
|
304
|
-
setIsRefreshingStatus(false)
|
|
305
|
-
}, [loadDetail, loadRows, selectedId, t])
|
|
321
|
+
}, [loadDetail, loadRows, runMutation, selectedId, t])
|
|
306
322
|
|
|
307
323
|
const handleCapturePayment = React.useCallback(async () => {
|
|
308
324
|
if (!selectedId) return
|
|
@@ -14,6 +14,7 @@ import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato
|
|
|
14
14
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
15
15
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
16
16
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
17
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
17
18
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
18
19
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
19
20
|
import { mapOfferRow, renderOfferPriceSummary, type OfferRow } from '@open-mercato/core/modules/sales/components/channels/offerTableUtils'
|
|
@@ -26,9 +27,27 @@ type OffersResponse = {
|
|
|
26
27
|
|
|
27
28
|
const PAGE_SIZE = 25
|
|
28
29
|
|
|
30
|
+
const SAVE_CONTEXT_ID = 'sales-channel-offers-list'
|
|
31
|
+
|
|
29
32
|
export default function SalesChannelOffersListPage() {
|
|
30
33
|
const t = useT()
|
|
31
34
|
const router = useRouter()
|
|
35
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
36
|
+
formId: string
|
|
37
|
+
resourceKind: string
|
|
38
|
+
retryLastMutation: () => Promise<boolean>
|
|
39
|
+
}>({
|
|
40
|
+
contextId: SAVE_CONTEXT_ID,
|
|
41
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
42
|
+
})
|
|
43
|
+
const mutationContext = React.useMemo(
|
|
44
|
+
() => ({
|
|
45
|
+
formId: SAVE_CONTEXT_ID,
|
|
46
|
+
resourceKind: 'sales.channels.offers',
|
|
47
|
+
retryLastMutation,
|
|
48
|
+
}),
|
|
49
|
+
[retryLastMutation],
|
|
50
|
+
)
|
|
32
51
|
const scopeVersion = useOrganizationScopeVersion()
|
|
33
52
|
const channelOptionsRef = React.useRef<Map<string, FilterOption>>(new Map())
|
|
34
53
|
const [rows, setRows] = React.useState<OfferRow[]>([])
|
|
@@ -282,19 +301,24 @@ export default function SalesChannelOffersListPage() {
|
|
|
282
301
|
|
|
283
302
|
const handleDelete = React.useCallback(async (row: OfferRow) => {
|
|
284
303
|
try {
|
|
285
|
-
await
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
await runMutation({
|
|
305
|
+
operation: () =>
|
|
306
|
+
withScopedApiRequestHeaders(
|
|
307
|
+
buildOptimisticLockHeader(row.updatedAt),
|
|
308
|
+
() => deleteCrud('catalog/offers', row.id, {
|
|
309
|
+
errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
312
|
+
context: mutationContext,
|
|
313
|
+
mutationPayload: { action: 'delete', id: row.id },
|
|
314
|
+
})
|
|
291
315
|
flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')
|
|
292
316
|
handleRefresh()
|
|
293
317
|
} catch (err) {
|
|
294
318
|
if (surfaceRecordConflict(err, t)) { handleRefresh(); return }
|
|
295
319
|
console.error('sales.channels.offers.delete', err)
|
|
296
320
|
}
|
|
297
|
-
}, [handleRefresh, t])
|
|
321
|
+
}, [handleRefresh, mutationContext, runMutation, t])
|
|
298
322
|
|
|
299
323
|
const tableTitle = (
|
|
300
324
|
<div className="flex flex-col gap-1">
|
|
@@ -13,6 +13,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
13
13
|
import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
15
15
|
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
16
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
16
17
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
17
18
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
19
|
|
|
@@ -34,9 +35,27 @@ type ChannelsResponse = {
|
|
|
34
35
|
|
|
35
36
|
const PAGE_SIZE = 25
|
|
36
37
|
|
|
38
|
+
const SAVE_CONTEXT_ID = 'sales-channels-list'
|
|
39
|
+
|
|
37
40
|
export default function SalesChannelsPage() {
|
|
38
41
|
const t = useT()
|
|
39
42
|
const router = useRouter()
|
|
43
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
44
|
+
formId: string
|
|
45
|
+
resourceKind: string
|
|
46
|
+
retryLastMutation: () => Promise<boolean>
|
|
47
|
+
}>({
|
|
48
|
+
contextId: SAVE_CONTEXT_ID,
|
|
49
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
50
|
+
})
|
|
51
|
+
const mutationContext = React.useMemo(
|
|
52
|
+
() => ({
|
|
53
|
+
formId: SAVE_CONTEXT_ID,
|
|
54
|
+
resourceKind: 'sales.channels',
|
|
55
|
+
retryLastMutation,
|
|
56
|
+
}),
|
|
57
|
+
[retryLastMutation],
|
|
58
|
+
)
|
|
40
59
|
const scopeVersion = useOrganizationScopeVersion()
|
|
41
60
|
const [rows, setRows] = React.useState<ChannelRow[]>([])
|
|
42
61
|
const [page, setPage] = React.useState(1)
|
|
@@ -139,12 +158,17 @@ export default function SalesChannelsPage() {
|
|
|
139
158
|
|
|
140
159
|
const handleDelete = React.useCallback(async (row: ChannelRow) => {
|
|
141
160
|
try {
|
|
142
|
-
await
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
161
|
+
await runMutation({
|
|
162
|
+
operation: () =>
|
|
163
|
+
withScopedApiRequestHeaders(
|
|
164
|
+
buildOptimisticLockHeader(row.updatedAt),
|
|
165
|
+
() => deleteCrud('sales/channels', row.id, {
|
|
166
|
+
errorMessage: t('sales.channels.table.errors.delete', 'Failed to delete channel.'),
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
context: mutationContext,
|
|
170
|
+
mutationPayload: { action: 'delete', id: row.id },
|
|
171
|
+
})
|
|
148
172
|
flash(t('sales.channels.table.messages.deleted', 'Channel deleted.'), 'success')
|
|
149
173
|
handleRefresh()
|
|
150
174
|
} catch (err) {
|
|
@@ -161,7 +185,7 @@ export default function SalesChannelsPage() {
|
|
|
161
185
|
}
|
|
162
186
|
console.error('sales.channels.delete', err)
|
|
163
187
|
}
|
|
164
|
-
}, [handleRefresh, t])
|
|
188
|
+
}, [handleRefresh, mutationContext, runMutation, t])
|
|
165
189
|
|
|
166
190
|
return (
|
|
167
191
|
<Page>
|
|
@@ -18,6 +18,7 @@ import { Label } from '@open-mercato/ui/primitives/label'
|
|
|
18
18
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
19
19
|
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
20
20
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
21
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
21
22
|
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
22
23
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
23
24
|
import { AppearanceSelector } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'
|
|
@@ -75,10 +76,28 @@ const normalizeEntry = (raw: any): AdjustmentKind | null => {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
const SAVE_CONTEXT_ID = 'sales-adjustment-kind-settings'
|
|
80
|
+
|
|
78
81
|
export function AdjustmentKindSettings() {
|
|
79
82
|
const t = useT()
|
|
80
83
|
const scopeVersion = useOrganizationScopeVersion()
|
|
81
84
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
85
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
86
|
+
formId: string
|
|
87
|
+
resourceKind: string
|
|
88
|
+
retryLastMutation: () => Promise<boolean>
|
|
89
|
+
}>({
|
|
90
|
+
contextId: SAVE_CONTEXT_ID,
|
|
91
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
92
|
+
})
|
|
93
|
+
const mutationContext = React.useMemo(
|
|
94
|
+
() => ({
|
|
95
|
+
formId: SAVE_CONTEXT_ID,
|
|
96
|
+
resourceKind: 'sales.adjustmentKinds',
|
|
97
|
+
retryLastMutation,
|
|
98
|
+
}),
|
|
99
|
+
[retryLastMutation],
|
|
100
|
+
)
|
|
82
101
|
const [items, setItems] = React.useState<AdjustmentKind[]>([])
|
|
83
102
|
const [search, setSearch] = React.useState('')
|
|
84
103
|
const [loading, setLoading] = React.useState(false)
|
|
@@ -222,16 +241,26 @@ export function AdjustmentKindSettings() {
|
|
|
222
241
|
|
|
223
242
|
const lockHeader =
|
|
224
243
|
dialog.mode === 'edit' ? buildOptimisticLockHeader(dialog.entry.updatedAt) : {}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
await runMutation({
|
|
245
|
+
operation: async () => {
|
|
246
|
+
const call = await withScopedApiRequestHeaders(lockHeader, () =>
|
|
247
|
+
apiCall('/api/sales/adjustment-kinds', {
|
|
248
|
+
method,
|
|
249
|
+
headers: { 'content-type': 'application/json' },
|
|
250
|
+
body,
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
if (!call.ok) {
|
|
254
|
+
await raiseCrudError(call.response, labels.saveError)
|
|
255
|
+
}
|
|
256
|
+
return call
|
|
257
|
+
},
|
|
258
|
+
context: mutationContext,
|
|
259
|
+
mutationPayload:
|
|
260
|
+
dialog.mode === 'create'
|
|
261
|
+
? { action: 'create', ...payload }
|
|
262
|
+
: { action: 'update', id: dialog.entry.id, ...payload },
|
|
263
|
+
})
|
|
235
264
|
flash(dialog.mode === 'create' ? labels.created : labels.updated, 'success')
|
|
236
265
|
closeDialog()
|
|
237
266
|
await loadItems()
|
|
@@ -242,7 +271,7 @@ export function AdjustmentKindSettings() {
|
|
|
242
271
|
} finally {
|
|
243
272
|
setSubmitting(false)
|
|
244
273
|
}
|
|
245
|
-
}, [closeDialog, dialog, form.color, form.icon, form.label, form.value, labels, loadItems])
|
|
274
|
+
}, [closeDialog, dialog, form.color, form.icon, form.label, form.value, labels, loadItems, mutationContext, runMutation])
|
|
246
275
|
|
|
247
276
|
const handleDelete = React.useCallback(
|
|
248
277
|
async (entry: AdjustmentKind) => {
|
|
@@ -253,18 +282,25 @@ export function AdjustmentKindSettings() {
|
|
|
253
282
|
})
|
|
254
283
|
if (!confirmed) return
|
|
255
284
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
285
|
+
await runMutation({
|
|
286
|
+
operation: async () => {
|
|
287
|
+
const call = await withScopedApiRequestHeaders(
|
|
288
|
+
buildOptimisticLockHeader(entry.updatedAt),
|
|
289
|
+
() =>
|
|
290
|
+
apiCall('/api/sales/adjustment-kinds', {
|
|
291
|
+
method: 'DELETE',
|
|
292
|
+
headers: { 'content-type': 'application/json' },
|
|
293
|
+
body: JSON.stringify({ id: entry.id }),
|
|
294
|
+
})
|
|
295
|
+
)
|
|
296
|
+
if (!call.ok) {
|
|
297
|
+
await raiseCrudError(call.response, labels.deleteError)
|
|
298
|
+
}
|
|
299
|
+
return call
|
|
300
|
+
},
|
|
301
|
+
context: mutationContext,
|
|
302
|
+
mutationPayload: { action: 'delete', id: entry.id },
|
|
303
|
+
})
|
|
268
304
|
flash(labels.deleted, 'success')
|
|
269
305
|
await loadItems()
|
|
270
306
|
} catch (err) {
|
|
@@ -273,7 +309,7 @@ export function AdjustmentKindSettings() {
|
|
|
273
309
|
flash(message, 'error')
|
|
274
310
|
}
|
|
275
311
|
},
|
|
276
|
-
[confirm, labels, loadItems]
|
|
312
|
+
[confirm, labels, loadItems, mutationContext, runMutation]
|
|
277
313
|
)
|
|
278
314
|
|
|
279
315
|
const formKeyHandler = React.useCallback(
|
|
@@ -6,6 +6,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
6
6
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
7
7
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
8
8
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
9
10
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
11
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
11
12
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -56,6 +57,8 @@ const normalizeState = (payload?: Partial<SettingsResponse> | null): FormState =
|
|
|
56
57
|
: '1',
|
|
57
58
|
})
|
|
58
59
|
|
|
60
|
+
const SAVE_CONTEXT_ID = 'sales-document-number-settings'
|
|
61
|
+
|
|
59
62
|
export function DocumentNumberSettings() {
|
|
60
63
|
const t = useT()
|
|
61
64
|
const scopeVersion = useOrganizationScopeVersion()
|
|
@@ -64,6 +67,15 @@ export function DocumentNumberSettings() {
|
|
|
64
67
|
const [saving, setSaving] = React.useState(false)
|
|
65
68
|
const [tokens, setTokens] = React.useState(DOCUMENT_NUMBER_TOKENS)
|
|
66
69
|
|
|
70
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
71
|
+
formId: string
|
|
72
|
+
resourceKind: string
|
|
73
|
+
retryLastMutation: () => Promise<boolean>
|
|
74
|
+
}>({
|
|
75
|
+
contextId: SAVE_CONTEXT_ID,
|
|
76
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
77
|
+
})
|
|
78
|
+
|
|
67
79
|
const translations = React.useMemo(() => ({
|
|
68
80
|
title: t('sales.config.numbering.title', 'Document numbers'),
|
|
69
81
|
description: t(
|
|
@@ -128,16 +140,26 @@ export function DocumentNumberSettings() {
|
|
|
128
140
|
orderNextNumber: Number.parseInt(formState.orderNextNumber, 10) || undefined,
|
|
129
141
|
quoteNextNumber: Number.parseInt(formState.quoteNextNumber, 10) || undefined,
|
|
130
142
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
const call = await runMutation({
|
|
144
|
+
// optimistic-lock-exempt: single-row tenant numbering settings blob — no per-record version / concurrent record edit
|
|
145
|
+
operation: async () => {
|
|
146
|
+
const response = await apiCall<SettingsResponse>('/api/sales/settings/document-numbers', {
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
headers: { 'content-type': 'application/json' },
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
})
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(translations.errors.save)
|
|
153
|
+
}
|
|
154
|
+
return response
|
|
155
|
+
},
|
|
156
|
+
context: {
|
|
157
|
+
formId: SAVE_CONTEXT_ID,
|
|
158
|
+
resourceKind: 'sales.settings',
|
|
159
|
+
retryLastMutation,
|
|
160
|
+
},
|
|
161
|
+
mutationPayload: payload,
|
|
136
162
|
})
|
|
137
|
-
if (!call.ok) {
|
|
138
|
-
flash(translations.errors.save, 'error')
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
163
|
setFormState(normalizeState(call.result))
|
|
142
164
|
setTokens(Array.isArray(call.result?.tokens) && call.result.tokens.length ? call.result.tokens : DOCUMENT_NUMBER_TOKENS)
|
|
143
165
|
flash(translations.messages.saved, 'success')
|
|
@@ -147,7 +169,7 @@ export function DocumentNumberSettings() {
|
|
|
147
169
|
} finally {
|
|
148
170
|
setSaving(false)
|
|
149
171
|
}
|
|
150
|
-
}, [formState.orderNextNumber, formState.orderNumberFormat, formState.quoteNextNumber, formState.quoteNumberFormat, translations.errors.save, translations.messages.saved])
|
|
172
|
+
}, [formState.orderNextNumber, formState.orderNumberFormat, formState.quoteNextNumber, formState.quoteNumberFormat, retryLastMutation, runMutation, translations.errors.save, translations.messages.saved])
|
|
151
173
|
|
|
152
174
|
const handleReset = React.useCallback(() => {
|
|
153
175
|
setFormState(DEFAULT_STATE)
|
|
@@ -6,6 +6,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
6
6
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
7
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
8
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
9
10
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
10
11
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
12
|
|
|
@@ -35,6 +36,8 @@ const normalizeStatusList = (list: unknown): string[] | null => {
|
|
|
35
36
|
return Array.from(set)
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const SAVE_CONTEXT_ID = 'sales-order-editing-settings'
|
|
40
|
+
|
|
38
41
|
export function OrderEditingSettings() {
|
|
39
42
|
const t = useT()
|
|
40
43
|
const scopeVersion = useOrganizationScopeVersion()
|
|
@@ -44,6 +47,15 @@ export function OrderEditingSettings() {
|
|
|
44
47
|
const [customerStatuses, setCustomerStatuses] = React.useState<string[] | null>(null)
|
|
45
48
|
const [addressStatuses, setAddressStatuses] = React.useState<string[] | null>(null)
|
|
46
49
|
|
|
50
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
51
|
+
formId: string
|
|
52
|
+
resourceKind: string
|
|
53
|
+
retryLastMutation: () => Promise<boolean>
|
|
54
|
+
}>({
|
|
55
|
+
contextId: SAVE_CONTEXT_ID,
|
|
56
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
57
|
+
})
|
|
58
|
+
|
|
47
59
|
const translations = React.useMemo(
|
|
48
60
|
() => ({
|
|
49
61
|
title: t('sales.config.orderEditing.title', 'Order editing guards'),
|
|
@@ -121,16 +133,26 @@ export function OrderEditingSettings() {
|
|
|
121
133
|
orderCustomerEditableStatuses: customerStatuses,
|
|
122
134
|
orderAddressEditableStatuses: addressStatuses,
|
|
123
135
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const call = await runMutation({
|
|
137
|
+
// optimistic-lock-exempt: single-row tenant order-editing settings blob — no per-record version / concurrent record edit
|
|
138
|
+
operation: async () => {
|
|
139
|
+
const response = await apiCall<SettingsResponse>('/api/sales/settings/order-editing', {
|
|
140
|
+
method: 'PUT',
|
|
141
|
+
headers: { 'content-type': 'application/json' },
|
|
142
|
+
body: JSON.stringify(payload),
|
|
143
|
+
})
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(translations.messages.saveError)
|
|
146
|
+
}
|
|
147
|
+
return response
|
|
148
|
+
},
|
|
149
|
+
context: {
|
|
150
|
+
formId: SAVE_CONTEXT_ID,
|
|
151
|
+
resourceKind: 'sales.settings',
|
|
152
|
+
retryLastMutation,
|
|
153
|
+
},
|
|
154
|
+
mutationPayload: payload,
|
|
129
155
|
})
|
|
130
|
-
if (!call.ok) {
|
|
131
|
-
flash(translations.messages.saveError, 'error')
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
156
|
setCustomerStatuses(normalizeStatusList(call.result?.orderCustomerEditableStatuses))
|
|
135
157
|
setAddressStatuses(normalizeStatusList(call.result?.orderAddressEditableStatuses))
|
|
136
158
|
setOptions(Array.isArray(call.result?.orderStatuses) ? call.result.orderStatuses : [])
|
|
@@ -141,7 +163,7 @@ export function OrderEditingSettings() {
|
|
|
141
163
|
} finally {
|
|
142
164
|
setSaving(false)
|
|
143
165
|
}
|
|
144
|
-
}, [addressStatuses, customerStatuses, translations.messages.saveError, translations.messages.saved])
|
|
166
|
+
}, [addressStatuses, customerStatuses, retryLastMutation, runMutation, translations.messages.saveError, translations.messages.saved])
|
|
145
167
|
|
|
146
168
|
const renderStatusList = React.useCallback(
|
|
147
169
|
(kind: 'customer' | 'address', values: string[] | null, label: string) => {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
11
11
|
import { apiCall, 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'
|
|
@@ -43,6 +44,8 @@ const DEFAULT_FORM_VALUES: DictionaryFormValues = {
|
|
|
43
44
|
icon: null,
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const SAVE_CONTEXT_ID = 'sales-status-settings'
|
|
48
|
+
|
|
46
49
|
export function StatusSettings() {
|
|
47
50
|
const t = useT()
|
|
48
51
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
@@ -51,6 +54,23 @@ export function StatusSettings() {
|
|
|
51
54
|
return value === key ? fallback : value
|
|
52
55
|
}, [t])
|
|
53
56
|
|
|
57
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
58
|
+
formId: string
|
|
59
|
+
resourceKind: string
|
|
60
|
+
retryLastMutation: () => Promise<boolean>
|
|
61
|
+
}>({
|
|
62
|
+
contextId: SAVE_CONTEXT_ID,
|
|
63
|
+
blockedMessage: translate('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
64
|
+
})
|
|
65
|
+
const mutationContext = React.useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
formId: SAVE_CONTEXT_ID,
|
|
68
|
+
resourceKind: 'sales.statuses',
|
|
69
|
+
retryLastMutation,
|
|
70
|
+
}),
|
|
71
|
+
[retryLastMutation],
|
|
72
|
+
)
|
|
73
|
+
|
|
54
74
|
const sections = React.useMemo<SectionDefinition[]>(() => [
|
|
55
75
|
{
|
|
56
76
|
kind: 'order-statuses',
|
|
@@ -198,18 +218,25 @@ export function StatusSettings() {
|
|
|
198
218
|
})
|
|
199
219
|
if (!confirmed) return
|
|
200
220
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
await runMutation({
|
|
222
|
+
operation: async () => {
|
|
223
|
+
const call = await withScopedApiRequestHeaders(
|
|
224
|
+
buildOptimisticLockHeader(entry.updatedAt),
|
|
225
|
+
() =>
|
|
226
|
+
apiCall(apiPaths[kind], {
|
|
227
|
+
method: 'DELETE',
|
|
228
|
+
headers: { 'content-type': 'application/json' },
|
|
229
|
+
body: JSON.stringify({ id: entry.id }),
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
if (!call.ok) {
|
|
233
|
+
await raiseCrudError(call.response, translate('sales.config.statuses.error.delete', 'Failed to delete status.'))
|
|
234
|
+
}
|
|
235
|
+
return call
|
|
236
|
+
},
|
|
237
|
+
context: mutationContext,
|
|
238
|
+
mutationPayload: { action: 'delete', kind, id: entry.id },
|
|
239
|
+
})
|
|
213
240
|
flash(translate('sales.config.statuses.success.delete', 'Status deleted.'), 'success')
|
|
214
241
|
await loadEntries(kind)
|
|
215
242
|
} catch (err) {
|
|
@@ -217,7 +244,7 @@ export function StatusSettings() {
|
|
|
217
244
|
const message = err instanceof Error ? err.message : translate('sales.config.statuses.error.delete', 'Failed to delete status.')
|
|
218
245
|
flash(message, 'error')
|
|
219
246
|
}
|
|
220
|
-
}, [apiPaths, confirm, loadEntries, translate])
|
|
247
|
+
}, [apiPaths, confirm, loadEntries, mutationContext, runMutation, translate])
|
|
221
248
|
|
|
222
249
|
const submitForm = React.useCallback(async (values: DictionaryFormValues) => {
|
|
223
250
|
if (!dialog) return
|
|
@@ -225,14 +252,21 @@ export function StatusSettings() {
|
|
|
225
252
|
setSubmitting(true)
|
|
226
253
|
try {
|
|
227
254
|
if (dialog.mode === 'create') {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
255
|
+
await runMutation({
|
|
256
|
+
operation: async () => {
|
|
257
|
+
const call = await apiCall(path, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: { 'content-type': 'application/json' },
|
|
260
|
+
body: JSON.stringify(values),
|
|
261
|
+
})
|
|
262
|
+
if (!call.ok) {
|
|
263
|
+
await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))
|
|
264
|
+
}
|
|
265
|
+
return call
|
|
266
|
+
},
|
|
267
|
+
context: mutationContext,
|
|
268
|
+
mutationPayload: { action: 'create', kind: dialog.kind, ...values },
|
|
232
269
|
})
|
|
233
|
-
if (!call.ok) {
|
|
234
|
-
await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))
|
|
235
|
-
}
|
|
236
270
|
flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')
|
|
237
271
|
} else if (dialog.mode === 'edit') {
|
|
238
272
|
const entry = dialog.entry
|
|
@@ -243,18 +277,25 @@ export function StatusSettings() {
|
|
|
243
277
|
if (nextColor !== (entry.color ?? null)) body.color = nextColor
|
|
244
278
|
const nextIcon = values.icon ?? null
|
|
245
279
|
if (nextIcon !== (entry.icon ?? null)) body.icon = nextIcon
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
await runMutation({
|
|
281
|
+
operation: async () => {
|
|
282
|
+
const call = await withScopedApiRequestHeaders(
|
|
283
|
+
buildOptimisticLockHeader(entry.updatedAt),
|
|
284
|
+
() =>
|
|
285
|
+
apiCall(path, {
|
|
286
|
+
method: 'PUT',
|
|
287
|
+
headers: { 'content-type': 'application/json' },
|
|
288
|
+
body: JSON.stringify(body),
|
|
289
|
+
})
|
|
290
|
+
)
|
|
291
|
+
if (!call.ok) {
|
|
292
|
+
await raiseCrudError(call.response, translate('sales.config.statuses.error.save', 'Failed to save status.'))
|
|
293
|
+
}
|
|
294
|
+
return call
|
|
295
|
+
},
|
|
296
|
+
context: mutationContext,
|
|
297
|
+
mutationPayload: { action: 'update', kind: dialog.kind, ...body },
|
|
298
|
+
})
|
|
258
299
|
flash(translate('sales.config.statuses.success.save', 'Status saved.'), 'success')
|
|
259
300
|
}
|
|
260
301
|
closeDialog()
|
|
@@ -267,7 +308,7 @@ export function StatusSettings() {
|
|
|
267
308
|
} finally {
|
|
268
309
|
setSubmitting(false)
|
|
269
310
|
}
|
|
270
|
-
}, [apiPaths, closeDialog, dialog, loadEntries, translate])
|
|
311
|
+
}, [apiPaths, closeDialog, dialog, loadEntries, mutationContext, runMutation, translate])
|
|
271
312
|
|
|
272
313
|
const currentValues = React.useMemo<DictionaryFormValues>(() => {
|
|
273
314
|
if (dialog && dialog.mode === 'edit') {
|