@open-mercato/core 0.6.6-develop.6205.1.109e4b6a84 → 0.6.6-develop.6228.1.cf483c68b5
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/currencies/backend/currencies/[id]/page.js +33 -8
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/page.js +63 -27
- package/dist/modules/currencies/backend/currencies/page.js.map +2 -2
- package/dist/modules/currencies/backend/exchange-rates/page.js +35 -14
- package/dist/modules/currencies/backend/exchange-rates/page.js.map +2 -2
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js +104 -41
- package/dist/modules/currencies/components/CurrencyFetchingConfig.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js +57 -8
- package/dist/modules/customer_accounts/api/admin/roles/[id]/acl.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +5 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js +30 -14
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/page.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +29 -19
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +166 -64
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +2 -2
- package/dist/modules/customers/components/AddressFormatSettings.js +31 -16
- package/dist/modules/customers/components/AddressFormatSettings.js.map +2 -2
- package/dist/modules/customers/components/DictionarySettings.js +57 -31
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +156 -76
- package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/AgendaList.js +10 -4
- package/dist/modules/customers/components/calendar/AgendaList.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +18 -2
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js +5 -2
- package/dist/modules/customers/components/calendar/MonthGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +6 -0
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js +2 -0
- package/dist/modules/customers/components/calendar/editor/RelatedToField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/lib/calendar/labels.js +33 -0
- package/dist/modules/customers/lib/calendar/labels.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +32 -11
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/[id]/route.js +59 -0
- package/dist/modules/data_sync/api/mappings/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/mappings/route.js +44 -0
- package/dist/modules/data_sync/api/mappings/route.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +31 -0
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/cancel.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/cancel.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +31 -0
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +59 -3
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +33 -4
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/dictionaries/fields/dictionary.js +29 -4
- package/dist/modules/dictionaries/fields/dictionary.js.map +2 -2
- package/dist/modules/directory/api/organization-branding/route.js +54 -2
- package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
- package/dist/modules/directory/backend/directory/branding/page.js +15 -9
- package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/organizations/page.js +28 -9
- package/dist/modules/directory/backend/directory/organizations/page.js.map +2 -2
- package/dist/modules/directory/backend/directory/tenants/page.js +29 -13
- package/dist/modules/directory/backend/directory/tenants/page.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +15 -0
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/definitions.js +26 -0
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/definitions.mutation-guard.js +57 -0
- package/dist/modules/entities/api/definitions.mutation-guard.js.map +7 -0
- package/dist/modules/entities/api/definitions.restore.js +15 -0
- package/dist/modules/entities/api/definitions.restore.js.map +2 -2
- package/dist/modules/entities/api/entities.js +31 -3
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +18 -0
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +28 -10
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/feature_toggles/api/overrides/route.js +38 -1
- package/dist/modules/feature_toggles/api/overrides/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/emails/[id]/route.js +30 -0
- package/dist/modules/inbox_ops/api/emails/[id]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js +30 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/accept-all/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.js.map +2 -2
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js +31 -0
- package/dist/modules/inbox_ops/api/proposals/[id]/categorize/route.js.map +2 -2
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js +68 -39
- package/dist/modules/messages/components/message-detail/hooks/useMessageDetailsActions.js.map +2 -2
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js +14 -4
- package/dist/modules/notifications/frontend/NotificationSettingsPageClient.js.map +2 -2
- package/dist/modules/payment_gateways/api/cancel/route.js +37 -0
- package/dist/modules/payment_gateways/api/cancel/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +37 -0
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/guards.js +31 -0
- package/dist/modules/payment_gateways/api/guards.js.map +7 -0
- package/dist/modules/payment_gateways/api/refund/route.js +37 -0
- package/dist/modules/payment_gateways/api/refund/route.js.map +2 -2
- package/dist/modules/payment_gateways/api/sessions/route.js +37 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/planner/api/availability-date-specific.js +11 -1
- package/dist/modules/planner/api/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js +20 -3
- package/dist/modules/planner/backend/planner/availability-rulesets/page.js.map +2 -2
- package/dist/modules/planner/commands/availability-date-specific.js +16 -0
- package/dist/modules/planner/commands/availability-date-specific.js.map +2 -2
- package/dist/modules/planner/components/AvailabilityRulesEditor.js +109 -40
- package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
- package/dist/modules/query_index/api/purge.js +35 -3
- package/dist/modules/query_index/api/purge.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +41 -3
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +57 -24
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +31 -0
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +55 -0
- package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +6 -1
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +28 -8
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipping-service.js +37 -7
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/workers/status-poller.js +1 -1
- package/dist/modules/shipping_carriers/workers/status-poller.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +49 -20
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +40 -10
- package/src/modules/currencies/backend/currencies/page.tsx +68 -29
- package/src/modules/currencies/backend/exchange-rates/page.tsx +40 -15
- package/src/modules/currencies/components/CurrencyFetchingConfig.tsx +110 -41
- package/src/modules/customer_accounts/api/admin/roles/[id]/acl.ts +69 -7
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +16 -8
- package/src/modules/customer_accounts/backend/customer_accounts/roles/page.tsx +32 -14
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +32 -20
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +171 -64
- package/src/modules/customers/components/AddressFormatSettings.tsx +39 -19
- package/src/modules/customers/components/DictionarySettings.tsx +63 -29
- package/src/modules/customers/components/PipelineSettings.tsx +165 -80
- package/src/modules/customers/components/calendar/AgendaList.tsx +13 -8
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +16 -2
- package/src/modules/customers/components/calendar/MonthGrid.tsx +5 -2
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +6 -0
- package/src/modules/customers/components/calendar/editor/RelatedToField.tsx +2 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +7 -0
- package/src/modules/customers/i18n/de.json +11 -7
- package/src/modules/customers/i18n/en.json +6 -2
- package/src/modules/customers/i18n/es.json +11 -7
- package/src/modules/customers/i18n/pl.json +11 -7
- package/src/modules/customers/lib/calendar/labels.ts +42 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +39 -11
- package/src/modules/data_sync/api/mappings/[id]/route.ts +63 -0
- package/src/modules/data_sync/api/mappings/route.ts +48 -0
- package/src/modules/data_sync/api/run.ts +33 -0
- package/src/modules/data_sync/api/runs/[id]/cancel.ts +34 -0
- package/src/modules/data_sync/api/runs/[id]/retry.ts +33 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +64 -2
- package/src/modules/data_sync/api/schedules/route.ts +36 -4
- package/src/modules/dictionaries/fields/dictionary.tsx +31 -8
- package/src/modules/dictionaries/i18n/de.json +1 -0
- package/src/modules/dictionaries/i18n/en.json +1 -0
- package/src/modules/dictionaries/i18n/es.json +1 -0
- package/src/modules/dictionaries/i18n/pl.json +1 -0
- package/src/modules/directory/api/organization-branding/route.ts +61 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +16 -10
- package/src/modules/directory/backend/directory/organizations/page.tsx +35 -8
- package/src/modules/directory/backend/directory/tenants/page.tsx +37 -13
- package/src/modules/entities/api/definitions.batch.ts +17 -0
- package/src/modules/entities/api/definitions.mutation-guard.ts +80 -0
- package/src/modules/entities/api/definitions.restore.ts +17 -0
- package/src/modules/entities/api/definitions.ts +30 -0
- package/src/modules/entities/api/entities.ts +35 -3
- package/src/modules/entities/api/records.ts +20 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +33 -10
- package/src/modules/feature_toggles/api/overrides/route.ts +44 -1
- package/src/modules/inbox_ops/api/emails/[id]/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/accept-all/route.ts +32 -0
- package/src/modules/inbox_ops/api/proposals/[id]/actions/[actionId]/route.ts +33 -0
- package/src/modules/inbox_ops/api/proposals/[id]/categorize/route.ts +33 -0
- package/src/modules/messages/components/message-detail/hooks/useMessageDetailsActions.ts +80 -42
- package/src/modules/notifications/frontend/NotificationSettingsPageClient.tsx +21 -4
- package/src/modules/payment_gateways/api/cancel/route.ts +39 -0
- package/src/modules/payment_gateways/api/capture/route.ts +39 -0
- package/src/modules/payment_gateways/api/guards.ts +59 -0
- package/src/modules/payment_gateways/api/refund/route.ts +39 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +40 -0
- package/src/modules/planner/api/availability-date-specific.ts +10 -0
- package/src/modules/planner/backend/planner/availability-rulesets/page.tsx +26 -5
- package/src/modules/planner/commands/availability-date-specific.ts +24 -0
- package/src/modules/planner/components/AvailabilityRulesEditor.tsx +122 -41
- package/src/modules/query_index/api/purge.ts +37 -3
- package/src/modules/query_index/api/reindex.ts +43 -3
- package/src/modules/query_index/components/QueryIndexesTable.tsx +66 -24
- package/src/modules/shipping_carriers/api/shipments/route.ts +31 -0
- package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +53 -0
- package/src/modules/shipping_carriers/data/validators.ts +5 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +29 -8
- package/src/modules/shipping_carriers/lib/shipping-service.ts +43 -7
- package/src/modules/shipping_carriers/workers/status-poller.ts +1 -1
- package/src/modules/translations/components/TranslationManager.tsx +65 -21
|
@@ -5,6 +5,7 @@ import type { UseQueryResult } from '@tanstack/react-query'
|
|
|
5
5
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import type { MessageActionsProps, MessageContentProps } from '@open-mercato/shared/modules/messages/types'
|
|
7
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
8
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
9
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
10
|
import type {
|
|
10
11
|
ActionResult,
|
|
@@ -21,6 +22,13 @@ type RequestAndRefreshOptions = {
|
|
|
21
22
|
|
|
22
23
|
type ConversationActionKind = 'archiveConversation' | 'deleteConversation' | 'markAllUnread'
|
|
23
24
|
|
|
25
|
+
type MessageDetailMutationContext = {
|
|
26
|
+
resourceKind: 'message' | 'conversation'
|
|
27
|
+
messageId: string
|
|
28
|
+
action: string
|
|
29
|
+
retryLastMutation: () => Promise<boolean>
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
type UseMessageDetailsActionsInput = {
|
|
25
33
|
id: string
|
|
26
34
|
t: TranslateFn
|
|
@@ -51,6 +59,10 @@ export function useMessageDetailsActions({
|
|
|
51
59
|
const [deleteConfirmationOpen, setDeleteConfirmationOpen] = React.useState(false)
|
|
52
60
|
const [activeConversationAction, setActiveConversationAction] = React.useState<ConversationActionKind | null>(null)
|
|
53
61
|
|
|
62
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<MessageDetailMutationContext>({
|
|
63
|
+
contextId: 'messages-detail-actions',
|
|
64
|
+
})
|
|
65
|
+
|
|
54
66
|
const requestAndRefresh = React.useCallback(async (
|
|
55
67
|
url: string,
|
|
56
68
|
method: 'PUT' | 'DELETE',
|
|
@@ -58,18 +70,24 @@ export function useMessageDetailsActions({
|
|
|
58
70
|
) => {
|
|
59
71
|
setUpdatingState(true)
|
|
60
72
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
await runMutation({
|
|
74
|
+
operation: async () => {
|
|
75
|
+
const call = await apiCall<{ ok?: boolean }>(url, { method })
|
|
76
|
+
if (!call.ok) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
toErrorMessage(call.result)
|
|
79
|
+
?? t('messages.errors.stateChangeFailed', 'Failed to update message state.'),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
if (options?.skipDetailAutoMarkRead) {
|
|
83
|
+
await refreshDetailWithoutAutoMarkRead()
|
|
84
|
+
} else {
|
|
85
|
+
await detailQuery.refetch()
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
context: { resourceKind: 'message', messageId: id, action: 'state-change', retryLastMutation },
|
|
89
|
+
mutationPayload: { messageId: id, action: 'state-change', url, method },
|
|
90
|
+
})
|
|
73
91
|
} catch (err) {
|
|
74
92
|
flash(
|
|
75
93
|
err instanceof Error
|
|
@@ -80,17 +98,23 @@ export function useMessageDetailsActions({
|
|
|
80
98
|
} finally {
|
|
81
99
|
setUpdatingState(false)
|
|
82
100
|
}
|
|
83
|
-
}, [detailQuery, refreshDetailWithoutAutoMarkRead, t])
|
|
101
|
+
}, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])
|
|
84
102
|
|
|
85
103
|
const handleDelete = React.useCallback(async () => {
|
|
86
104
|
setUpdatingState(true)
|
|
87
105
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
await runMutation({
|
|
107
|
+
operation: async () => {
|
|
108
|
+
const call = await apiCall<{ ok?: boolean }>(`/api/messages/${encodeURIComponent(id)}`, {
|
|
109
|
+
method: 'DELETE',
|
|
110
|
+
})
|
|
111
|
+
if (!call.ok) {
|
|
112
|
+
throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
context: { resourceKind: 'message', messageId: id, action: 'delete', retryLastMutation },
|
|
116
|
+
mutationPayload: { messageId: id, action: 'delete' },
|
|
90
117
|
})
|
|
91
|
-
if (!call.ok) {
|
|
92
|
-
throw new Error(toErrorMessage(call.result) ?? t('messages.errors.deleteFailed', 'Failed to delete message.'))
|
|
93
|
-
}
|
|
94
118
|
flash(t('messages.flash.deleted', 'Message deleted.'), 'success')
|
|
95
119
|
onDeleted()
|
|
96
120
|
} catch (err) {
|
|
@@ -103,7 +127,7 @@ export function useMessageDetailsActions({
|
|
|
103
127
|
} finally {
|
|
104
128
|
setUpdatingState(false)
|
|
105
129
|
}
|
|
106
|
-
}, [id, onDeleted, t])
|
|
130
|
+
}, [id, onDeleted, retryLastMutation, runMutation, t])
|
|
107
131
|
|
|
108
132
|
const runConversationAction = React.useCallback(async (
|
|
109
133
|
action: ConversationActionKind,
|
|
@@ -117,13 +141,19 @@ export function useMessageDetailsActions({
|
|
|
117
141
|
) => {
|
|
118
142
|
setActiveConversationAction(action)
|
|
119
143
|
try {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
await runMutation({
|
|
145
|
+
operation: async () => {
|
|
146
|
+
const call = await apiCall<{ ok?: boolean; affectedCount?: number }>(options.url, { method: options.method })
|
|
147
|
+
if (!call.ok) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
toErrorMessage(call.result)
|
|
150
|
+
?? t('messages.errors.conversationActionFailed', 'Failed to update conversation.'),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
context: { resourceKind: 'conversation', messageId: id, action, retryLastMutation },
|
|
155
|
+
mutationPayload: { messageId: id, action, url: options.url, method: options.method },
|
|
156
|
+
})
|
|
127
157
|
|
|
128
158
|
flash(options.successMessage, 'success')
|
|
129
159
|
|
|
@@ -147,7 +177,7 @@ export function useMessageDetailsActions({
|
|
|
147
177
|
} finally {
|
|
148
178
|
setActiveConversationAction(null)
|
|
149
179
|
}
|
|
150
|
-
}, [detailQuery, refreshDetailWithoutAutoMarkRead, t])
|
|
180
|
+
}, [detailQuery, id, refreshDetailWithoutAutoMarkRead, retryLastMutation, runMutation, t])
|
|
151
181
|
|
|
152
182
|
const archiveConversation = React.useCallback(async (messageId?: string) => {
|
|
153
183
|
const targetMessageId = messageId ?? id
|
|
@@ -194,21 +224,29 @@ export function useMessageDetailsActions({
|
|
|
194
224
|
const executeAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {
|
|
195
225
|
setExecutingActionId(action.id)
|
|
196
226
|
try {
|
|
197
|
-
const call = await
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
const call = await runMutation({
|
|
228
|
+
operation: async () => {
|
|
229
|
+
const result = await apiCall<ActionResult>(
|
|
230
|
+
`/api/messages/${encodeURIComponent(id)}/actions/${encodeURIComponent(action.id)}`,
|
|
231
|
+
{
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
234
|
+
body: JSON.stringify(payload ?? {}),
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if (!result.ok) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
toErrorMessage(result.result)
|
|
241
|
+
?? t('messages.errors.actionFailed', 'Failed to execute action.'),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return result
|
|
203
246
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
throw new Error(
|
|
208
|
-
toErrorMessage(call.result)
|
|
209
|
-
?? t('messages.errors.actionFailed', 'Failed to execute action.'),
|
|
210
|
-
)
|
|
211
|
-
}
|
|
247
|
+
context: { resourceKind: 'message', messageId: id, action: 'execute-action', retryLastMutation },
|
|
248
|
+
mutationPayload: { messageId: id, action: 'execute-action', actionId: action.id },
|
|
249
|
+
})
|
|
212
250
|
|
|
213
251
|
flash(t('messages.flash.actionSuccess', 'Action completed.'), 'success')
|
|
214
252
|
|
|
@@ -233,7 +271,7 @@ export function useMessageDetailsActions({
|
|
|
233
271
|
} finally {
|
|
234
272
|
setExecutingActionId(null)
|
|
235
273
|
}
|
|
236
|
-
}, [detailQuery, id, t])
|
|
274
|
+
}, [detailQuery, id, retryLastMutation, runMutation, t])
|
|
237
275
|
|
|
238
276
|
const handleExecuteAction = React.useCallback(async (action: MessageAction, payload?: Record<string, unknown>) => {
|
|
239
277
|
if (executingActionId || detail?.actionTaken) return
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
5
|
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
6
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
6
7
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -35,6 +36,8 @@ const emptySettings: NotificationDeliveryConfig = {
|
|
|
35
36
|
},
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const SETTINGS_CONTEXT_ID = 'notifications-settings'
|
|
40
|
+
|
|
38
41
|
export function NotificationSettingsPageClient() {
|
|
39
42
|
const t = useT()
|
|
40
43
|
const [settings, setSettings] = React.useState<NotificationDeliveryConfig | null>(null)
|
|
@@ -42,6 +45,15 @@ export function NotificationSettingsPageClient() {
|
|
|
42
45
|
const [saving, setSaving] = React.useState(false)
|
|
43
46
|
const [error, setError] = React.useState<string | null>(null)
|
|
44
47
|
|
|
48
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
49
|
+
formId: string
|
|
50
|
+
resourceKind: string
|
|
51
|
+
retryLastMutation: () => Promise<boolean>
|
|
52
|
+
}>({
|
|
53
|
+
contextId: SETTINGS_CONTEXT_ID,
|
|
54
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
55
|
+
})
|
|
56
|
+
|
|
45
57
|
const fetchSettings = React.useCallback(async () => {
|
|
46
58
|
setLoading(true)
|
|
47
59
|
setError(null)
|
|
@@ -96,10 +108,15 @@ export function NotificationSettingsPageClient() {
|
|
|
96
108
|
if (!settings) return
|
|
97
109
|
setSaving(true)
|
|
98
110
|
try {
|
|
99
|
-
const response = await
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
const response = await runMutation({
|
|
112
|
+
operation: () =>
|
|
113
|
+
apiCall<SettingsResponse>('/api/notifications/settings', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify(settings),
|
|
117
|
+
}),
|
|
118
|
+
context: { formId: SETTINGS_CONTEXT_ID, resourceKind: 'notifications.settings', retryLastMutation },
|
|
119
|
+
mutationPayload: { settings },
|
|
103
120
|
})
|
|
104
121
|
if (!response.ok) {
|
|
105
122
|
const message = response.result?.error || t('notifications.settings.saveError', 'Failed to save notification settings')
|
|
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
|
5
5
|
import { cancelSchema } from '../../data/validators'
|
|
6
6
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
7
7
|
import { paymentGatewaysTag } from '../openapi'
|
|
8
|
+
import {
|
|
9
|
+
resolveUserFeatures,
|
|
10
|
+
runPaymentGatewayMutationGuardAfterSuccess,
|
|
11
|
+
runPaymentGatewayMutationGuards,
|
|
12
|
+
} from '../guards'
|
|
13
|
+
|
|
14
|
+
const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
|
|
8
15
|
|
|
9
16
|
export const metadata = {
|
|
10
17
|
path: '/payment_gateways/cancel',
|
|
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const container = await createRequestContainer()
|
|
34
|
+
const guardResult = await runPaymentGatewayMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? '',
|
|
40
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
41
|
+
resourceId: parsed.data.transactionId,
|
|
42
|
+
operation: 'update',
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth),
|
|
48
|
+
)
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
guardResult.errorBody ?? { error: 'Operation blocked by guard' },
|
|
52
|
+
{ status: guardResult.errorStatus ?? 422 },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
const service = container.resolve('paymentGatewayService') as PaymentGatewayService
|
|
28
57
|
|
|
29
58
|
try {
|
|
@@ -32,6 +61,16 @@ export async function POST(req: Request) {
|
|
|
32
61
|
parsed.data.reason,
|
|
33
62
|
{ organizationId: auth.orgId as string, tenantId: auth.tenantId },
|
|
34
63
|
)
|
|
64
|
+
await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
65
|
+
tenantId: auth.tenantId,
|
|
66
|
+
organizationId: auth.orgId,
|
|
67
|
+
userId: auth.sub ?? '',
|
|
68
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
69
|
+
resourceId: parsed.data.transactionId,
|
|
70
|
+
operation: 'update',
|
|
71
|
+
requestMethod: req.method,
|
|
72
|
+
requestHeaders: req.headers,
|
|
73
|
+
})
|
|
35
74
|
return NextResponse.json(result)
|
|
36
75
|
} catch (err: unknown) {
|
|
37
76
|
const message = err instanceof Error ? err.message : 'Cancel failed'
|
|
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
|
5
5
|
import { captureSchema } from '../../data/validators'
|
|
6
6
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
7
7
|
import { paymentGatewaysTag } from '../openapi'
|
|
8
|
+
import {
|
|
9
|
+
resolveUserFeatures,
|
|
10
|
+
runPaymentGatewayMutationGuardAfterSuccess,
|
|
11
|
+
runPaymentGatewayMutationGuards,
|
|
12
|
+
} from '../guards'
|
|
13
|
+
|
|
14
|
+
const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
|
|
8
15
|
|
|
9
16
|
export const metadata = {
|
|
10
17
|
path: '/payment_gateways/capture',
|
|
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const container = await createRequestContainer()
|
|
34
|
+
const guardResult = await runPaymentGatewayMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? '',
|
|
40
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
41
|
+
resourceId: parsed.data.transactionId,
|
|
42
|
+
operation: 'update',
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth),
|
|
48
|
+
)
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
guardResult.errorBody ?? { error: 'Operation blocked by guard' },
|
|
52
|
+
{ status: guardResult.errorStatus ?? 422 },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
const service = container.resolve('paymentGatewayService') as PaymentGatewayService
|
|
28
57
|
|
|
29
58
|
try {
|
|
@@ -32,6 +61,16 @@ export async function POST(req: Request) {
|
|
|
32
61
|
parsed.data.amount,
|
|
33
62
|
{ organizationId: auth.orgId as string, tenantId: auth.tenantId },
|
|
34
63
|
)
|
|
64
|
+
await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
65
|
+
tenantId: auth.tenantId,
|
|
66
|
+
organizationId: auth.orgId,
|
|
67
|
+
userId: auth.sub ?? '',
|
|
68
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
69
|
+
resourceId: parsed.data.transactionId,
|
|
70
|
+
operation: 'update',
|
|
71
|
+
requestMethod: req.method,
|
|
72
|
+
requestHeaders: req.headers,
|
|
73
|
+
})
|
|
35
74
|
return NextResponse.json(result)
|
|
36
75
|
} catch (err: unknown) {
|
|
37
76
|
const message = err instanceof Error ? err.message : 'Capture failed'
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AwilixContainer } from 'awilix'
|
|
2
|
+
import {
|
|
3
|
+
bridgeLegacyGuard,
|
|
4
|
+
runMutationGuards,
|
|
5
|
+
type MutationGuard,
|
|
6
|
+
type MutationGuardInput,
|
|
7
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard-registry'
|
|
8
|
+
|
|
9
|
+
type GuardAfterCallback = {
|
|
10
|
+
guard: MutationGuard
|
|
11
|
+
metadata: Record<string, unknown> | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveUserFeatures(auth: unknown): string[] {
|
|
15
|
+
const features = (auth as { features?: unknown })?.features
|
|
16
|
+
if (!Array.isArray(features)) return []
|
|
17
|
+
return features.filter((value): value is string => typeof value === 'string')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function runPaymentGatewayMutationGuards(
|
|
21
|
+
container: AwilixContainer,
|
|
22
|
+
input: MutationGuardInput,
|
|
23
|
+
userFeatures: string[],
|
|
24
|
+
): Promise<{
|
|
25
|
+
ok: boolean
|
|
26
|
+
errorBody?: Record<string, unknown>
|
|
27
|
+
errorStatus?: number
|
|
28
|
+
modifiedPayload?: Record<string, unknown>
|
|
29
|
+
afterSuccessCallbacks: GuardAfterCallback[]
|
|
30
|
+
}> {
|
|
31
|
+
const legacyGuard = bridgeLegacyGuard(container)
|
|
32
|
+
if (!legacyGuard) {
|
|
33
|
+
return { ok: true, afterSuccessCallbacks: [] }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return runMutationGuards([legacyGuard], input, { userFeatures })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runPaymentGatewayMutationGuardAfterSuccess(
|
|
40
|
+
callbacks: GuardAfterCallback[],
|
|
41
|
+
input: {
|
|
42
|
+
tenantId: string
|
|
43
|
+
organizationId: string | null
|
|
44
|
+
userId: string
|
|
45
|
+
resourceKind: string
|
|
46
|
+
resourceId: string
|
|
47
|
+
operation: 'create' | 'update' | 'delete'
|
|
48
|
+
requestMethod: string
|
|
49
|
+
requestHeaders: Headers
|
|
50
|
+
},
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
for (const callback of callbacks) {
|
|
53
|
+
if (!callback.guard.afterSuccess) continue
|
|
54
|
+
await callback.guard.afterSuccess({
|
|
55
|
+
...input,
|
|
56
|
+
metadata: callback.metadata ?? null,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
|
5
5
|
import { refundSchema } from '../../data/validators'
|
|
6
6
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
7
7
|
import { paymentGatewaysTag } from '../openapi'
|
|
8
|
+
import {
|
|
9
|
+
resolveUserFeatures,
|
|
10
|
+
runPaymentGatewayMutationGuardAfterSuccess,
|
|
11
|
+
runPaymentGatewayMutationGuards,
|
|
12
|
+
} from '../guards'
|
|
13
|
+
|
|
14
|
+
const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
|
|
8
15
|
|
|
9
16
|
export const metadata = {
|
|
10
17
|
path: '/payment_gateways/refund',
|
|
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const container = await createRequestContainer()
|
|
34
|
+
const guardResult = await runPaymentGatewayMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? '',
|
|
40
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
41
|
+
resourceId: parsed.data.transactionId,
|
|
42
|
+
operation: 'update',
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth),
|
|
48
|
+
)
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
guardResult.errorBody ?? { error: 'Operation blocked by guard' },
|
|
52
|
+
{ status: guardResult.errorStatus ?? 422 },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
const service = container.resolve('paymentGatewayService') as PaymentGatewayService
|
|
28
57
|
|
|
29
58
|
try {
|
|
@@ -33,6 +62,16 @@ export async function POST(req: Request) {
|
|
|
33
62
|
parsed.data.reason,
|
|
34
63
|
{ organizationId: auth.orgId as string, tenantId: auth.tenantId },
|
|
35
64
|
)
|
|
65
|
+
await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
66
|
+
tenantId: auth.tenantId,
|
|
67
|
+
organizationId: auth.orgId,
|
|
68
|
+
userId: auth.sub ?? '',
|
|
69
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
70
|
+
resourceId: parsed.data.transactionId,
|
|
71
|
+
operation: 'update',
|
|
72
|
+
requestMethod: req.method,
|
|
73
|
+
requestHeaders: req.headers,
|
|
74
|
+
})
|
|
36
75
|
return NextResponse.json(result)
|
|
37
76
|
} catch (err: unknown) {
|
|
38
77
|
const message = err instanceof Error ? err.message : 'Refund failed'
|
|
@@ -5,6 +5,13 @@ import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
|
5
5
|
import { createSessionSchema } from '../../data/validators'
|
|
6
6
|
import type { PaymentGatewayService } from '../../lib/gateway-service'
|
|
7
7
|
import { paymentGatewaysTag } from '../openapi'
|
|
8
|
+
import {
|
|
9
|
+
resolveUserFeatures,
|
|
10
|
+
runPaymentGatewayMutationGuardAfterSuccess,
|
|
11
|
+
runPaymentGatewayMutationGuards,
|
|
12
|
+
} from '../guards'
|
|
13
|
+
|
|
14
|
+
const gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'
|
|
8
15
|
|
|
9
16
|
export const metadata = {
|
|
10
17
|
path: '/payment_gateways/sessions',
|
|
@@ -24,6 +31,28 @@ export async function POST(req: Request) {
|
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
const container = await createRequestContainer()
|
|
34
|
+
const guardResult = await runPaymentGatewayMutationGuards(
|
|
35
|
+
container,
|
|
36
|
+
{
|
|
37
|
+
tenantId: auth.tenantId,
|
|
38
|
+
organizationId: auth.orgId,
|
|
39
|
+
userId: auth.sub ?? '',
|
|
40
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
41
|
+
resourceId: null,
|
|
42
|
+
operation: 'create',
|
|
43
|
+
requestMethod: req.method,
|
|
44
|
+
requestHeaders: req.headers,
|
|
45
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
46
|
+
},
|
|
47
|
+
resolveUserFeatures(auth),
|
|
48
|
+
)
|
|
49
|
+
if (!guardResult.ok) {
|
|
50
|
+
return NextResponse.json(
|
|
51
|
+
guardResult.errorBody ?? { error: 'Operation blocked by guard' },
|
|
52
|
+
{ status: guardResult.errorStatus ?? 422 },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
const service = container.resolve('paymentGatewayService') as PaymentGatewayService
|
|
28
57
|
|
|
29
58
|
try {
|
|
@@ -46,6 +75,17 @@ export async function POST(req: Request) {
|
|
|
46
75
|
const redirectUrl = session.redirectUrl
|
|
47
76
|
?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null)
|
|
48
77
|
|
|
78
|
+
await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {
|
|
79
|
+
tenantId: auth.tenantId,
|
|
80
|
+
organizationId: auth.orgId,
|
|
81
|
+
userId: auth.sub ?? '',
|
|
82
|
+
resourceKind: gatewayTransactionResourceKind,
|
|
83
|
+
resourceId: transaction.id,
|
|
84
|
+
operation: 'create',
|
|
85
|
+
requestMethod: req.method,
|
|
86
|
+
requestHeaders: req.headers,
|
|
87
|
+
})
|
|
88
|
+
|
|
49
89
|
return NextResponse.json({
|
|
50
90
|
transactionId: transaction.id,
|
|
51
91
|
sessionId: session.sessionId,
|
|
@@ -161,6 +161,16 @@ export const openApi = {
|
|
|
161
161
|
{ status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
|
|
162
162
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
163
163
|
{ status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
|
|
164
|
+
{
|
|
165
|
+
status: 409,
|
|
166
|
+
description: 'Conflict — the subject/date availability was modified by another edit',
|
|
167
|
+
schema: z.object({
|
|
168
|
+
error: z.string(),
|
|
169
|
+
code: z.string(),
|
|
170
|
+
currentUpdatedAt: z.string(),
|
|
171
|
+
expectedUpdatedAt: z.string(),
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
164
174
|
],
|
|
165
175
|
},
|
|
166
176
|
},
|
|
@@ -11,6 +11,7 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
|
11
11
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
12
12
|
import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
13
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
14
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
14
15
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
15
16
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
16
17
|
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
@@ -41,6 +42,23 @@ export default function PlannerAvailabilityRuleSetsPage() {
|
|
|
41
42
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
42
43
|
const router = useRouter()
|
|
43
44
|
const scopeVersion = useOrganizationScopeVersion()
|
|
45
|
+
const mutationContextId = 'planner-availability-rule-sets'
|
|
46
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
47
|
+
formId: string
|
|
48
|
+
resourceKind: string
|
|
49
|
+
retryLastMutation: () => Promise<boolean>
|
|
50
|
+
}>({
|
|
51
|
+
contextId: mutationContextId,
|
|
52
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
53
|
+
})
|
|
54
|
+
const mutationContext = React.useMemo(
|
|
55
|
+
() => ({
|
|
56
|
+
formId: mutationContextId,
|
|
57
|
+
resourceKind: 'planner.availability_rule_set',
|
|
58
|
+
retryLastMutation,
|
|
59
|
+
}),
|
|
60
|
+
[mutationContextId, retryLastMutation],
|
|
61
|
+
)
|
|
44
62
|
const [rows, setRows] = React.useState<RuleSetRow[]>([])
|
|
45
63
|
const [page, setPage] = React.useState(1)
|
|
46
64
|
const [total, setTotal] = React.useState(0)
|
|
@@ -127,10 +145,13 @@ export default function PlannerAvailabilityRuleSetsPage() {
|
|
|
127
145
|
})
|
|
128
146
|
if (!confirmed) return
|
|
129
147
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
await runMutation({
|
|
149
|
+
operation: () => withScopedApiRequestHeaders(buildOptimisticLockHeader(entry.updatedAt), () => (
|
|
150
|
+
deleteCrud('planner/availability-rule-sets', entry.id, { errorMessage: labels.errors.delete })
|
|
151
|
+
)),
|
|
152
|
+
context: mutationContext,
|
|
153
|
+
mutationPayload: { action: 'delete', id: entry.id },
|
|
154
|
+
})
|
|
134
155
|
flash(labels.messages.deleted, 'success')
|
|
135
156
|
handleRefresh()
|
|
136
157
|
} catch (error) {
|
|
@@ -138,7 +159,7 @@ export default function PlannerAvailabilityRuleSetsPage() {
|
|
|
138
159
|
const normalized = normalizeCrudServerError(error)
|
|
139
160
|
flash(normalized.message ?? labels.errors.delete, 'error')
|
|
140
161
|
}
|
|
141
|
-
}, [confirm, handleRefresh, labels.actions.deleteConfirm, labels.errors.delete, labels.messages.deleted])
|
|
162
|
+
}, [confirm, handleRefresh, labels.actions.deleteConfirm, labels.errors.delete, labels.messages.deleted, mutationContext, runMutation])
|
|
142
163
|
|
|
143
164
|
const columns = React.useMemo<ColumnDef<RuleSetRow>[]>(() => [
|
|
144
165
|
{
|
|
@@ -9,10 +9,28 @@ import {
|
|
|
9
9
|
type PlannerAvailabilityDateSpecificReplaceInput,
|
|
10
10
|
} from '../data/validators'
|
|
11
11
|
import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
|
|
12
|
+
import { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
12
13
|
import type { PlannerAvailabilityKind, PlannerAvailabilitySubjectType } from '../data/entities'
|
|
13
14
|
|
|
14
15
|
const AVAILABILITY_RULE_RESOURCE_KIND = 'planner.availability.rule'
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The date-specific aggregate (the one-off rules for a subject/date) has no
|
|
19
|
+
* single parent row, so its optimistic-lock version is the latest `updated_at`
|
|
20
|
+
* of the rules being replaced. The editor sends that token via the extension
|
|
21
|
+
* header; an empty set means "nothing to clobber" → no current version → the
|
|
22
|
+
* guard is a no-op (strictly additive, fail-open).
|
|
23
|
+
*/
|
|
24
|
+
function resolveAggregateLockVersion(rules: PlannerAvailabilityRule[]): Date | null {
|
|
25
|
+
let latest: Date | null = null
|
|
26
|
+
for (const rule of rules) {
|
|
27
|
+
const value = rule.updatedAt
|
|
28
|
+
if (!value || Number.isNaN(value.getTime())) continue
|
|
29
|
+
if (!latest || value.getTime() > latest.getTime()) latest = value
|
|
30
|
+
}
|
|
31
|
+
return latest
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
type AvailabilityRuleSnapshot = {
|
|
17
35
|
id: string
|
|
18
36
|
tenantId: string
|
|
@@ -207,6 +225,12 @@ const replaceDateSpecificAvailabilityCommand: CommandHandler<PlannerAvailability
|
|
|
207
225
|
if (window.repeat !== 'once') return false
|
|
208
226
|
return dates.has(formatDateKey(window.startAt))
|
|
209
227
|
})
|
|
228
|
+
enforceCommandOptimisticLock({
|
|
229
|
+
resourceKind: AVAILABILITY_RULE_RESOURCE_KIND,
|
|
230
|
+
resourceId: `${parsed.subjectType}:${parsed.subjectId}`,
|
|
231
|
+
current: resolveAggregateLockVersion(toDelete),
|
|
232
|
+
request: ctx.request ?? null,
|
|
233
|
+
})
|
|
210
234
|
toDelete.forEach((rule) => {
|
|
211
235
|
rule.deletedAt = now
|
|
212
236
|
rule.updatedAt = now
|