@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
|
@@ -3,7 +3,8 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
4
4
|
import { flash } from "@open-mercato/ui/backend/FlashMessages";
|
|
5
5
|
import { apiCall, withScopedApiRequestHeaders } from "@open-mercato/ui/backend/utils/apiCall";
|
|
6
|
-
import { buildOptimisticLockHeader } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
6
|
+
import { buildOptimisticLockHeader, extractOptimisticLockConflict } from "@open-mercato/ui/backend/utils/optimisticLock";
|
|
7
|
+
import { useGuardedMutation } from "@open-mercato/ui/backend/injection/useGuardedMutation";
|
|
7
8
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
8
9
|
import { Spinner } from "@open-mercato/ui/primitives/spinner";
|
|
9
10
|
import { Switch } from "@open-mercato/ui/primitives/switch";
|
|
@@ -15,23 +16,36 @@ function CurrencyFetchingConfig() {
|
|
|
15
16
|
const [fetching, setFetching] = useState(null);
|
|
16
17
|
const [initializing, setInitializing] = useState(false);
|
|
17
18
|
const availableProviders = useMemo(() => ["NBP", "Raiffeisen Bank Polska"], []);
|
|
19
|
+
const mutationContextId = "currencies-fetch-configs:mutation";
|
|
20
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({
|
|
21
|
+
contextId: mutationContextId,
|
|
22
|
+
blockedMessage: t("ui.forms.flash.saveBlocked", "Save blocked by validation")
|
|
23
|
+
});
|
|
18
24
|
const createProviderConfig = useCallback(async (provider) => {
|
|
19
25
|
try {
|
|
20
|
-
await
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
await runMutation({
|
|
27
|
+
operation: () => apiCall("/api/currencies/fetch-configs", {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
provider,
|
|
32
|
+
isEnabled: false,
|
|
33
|
+
syncTime: "09:00"
|
|
34
|
+
})
|
|
35
|
+
}),
|
|
36
|
+
context: {
|
|
37
|
+
formId: mutationContextId,
|
|
38
|
+
resourceKind: "currencies.fetch_config",
|
|
39
|
+
retryLastMutation
|
|
40
|
+
},
|
|
41
|
+
mutationPayload: { provider, isEnabled: false, syncTime: "09:00" }
|
|
28
42
|
});
|
|
29
43
|
} catch (err) {
|
|
30
44
|
if (!err.message?.includes("already configured")) {
|
|
31
45
|
throw err;
|
|
32
46
|
}
|
|
33
47
|
}
|
|
34
|
-
}, []);
|
|
48
|
+
}, [mutationContextId, retryLastMutation, runMutation]);
|
|
35
49
|
const initializeMissingProviders = useCallback(async (providers) => {
|
|
36
50
|
setInitializing(true);
|
|
37
51
|
try {
|
|
@@ -70,17 +84,36 @@ function CurrencyFetchingConfig() {
|
|
|
70
84
|
}, [availableProviders, initializeMissingProviders, t]);
|
|
71
85
|
const toggleEnabled = useCallback(async (configId, currentValue, updatedAt) => {
|
|
72
86
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const call = await runMutation({
|
|
88
|
+
operation: async () => {
|
|
89
|
+
const response = await withScopedApiRequestHeaders(
|
|
90
|
+
buildOptimisticLockHeader(updatedAt),
|
|
91
|
+
() => apiCall("/api/currencies/fetch-configs", {
|
|
92
|
+
method: "PUT",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
id: configId,
|
|
96
|
+
isEnabled: !currentValue
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw Object.assign(new Error("[internal] currencies.fetchConfig.toggle failed"), {
|
|
102
|
+
status: response.status,
|
|
103
|
+
...response.result ?? {}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return response;
|
|
107
|
+
},
|
|
108
|
+
context: {
|
|
109
|
+
formId: mutationContextId,
|
|
110
|
+
resourceKind: "currencies.fetch_config",
|
|
111
|
+
resourceId: configId,
|
|
112
|
+
retryLastMutation
|
|
113
|
+
},
|
|
114
|
+
mutationPayload: { id: configId, isEnabled: !currentValue }
|
|
115
|
+
});
|
|
116
|
+
const result = call.result;
|
|
84
117
|
if (result?.config) {
|
|
85
118
|
setConfigs(
|
|
86
119
|
(prev) => prev.map((c) => c.id === configId ? result.config : c)
|
|
@@ -91,41 +124,71 @@ function CurrencyFetchingConfig() {
|
|
|
91
124
|
);
|
|
92
125
|
}
|
|
93
126
|
} catch (err) {
|
|
127
|
+
if (extractOptimisticLockConflict(err)) return;
|
|
94
128
|
flash(err.message || t("currencies.fetch.error_update_config"), "error");
|
|
95
129
|
}
|
|
96
|
-
}, [t]);
|
|
130
|
+
}, [mutationContextId, retryLastMutation, runMutation, t]);
|
|
97
131
|
const updateSyncTime = useCallback(async (configId, syncTime, updatedAt) => {
|
|
98
132
|
try {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
const call = await runMutation({
|
|
134
|
+
operation: async () => {
|
|
135
|
+
const response = await withScopedApiRequestHeaders(
|
|
136
|
+
buildOptimisticLockHeader(updatedAt),
|
|
137
|
+
() => apiCall("/api/currencies/fetch-configs", {
|
|
138
|
+
method: "PUT",
|
|
139
|
+
headers: { "Content-Type": "application/json" },
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
id: configId,
|
|
142
|
+
syncTime: syncTime || null
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw Object.assign(new Error("[internal] currencies.fetchConfig.updateSyncTime failed"), {
|
|
148
|
+
status: response.status,
|
|
149
|
+
...response.result ?? {}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return response;
|
|
153
|
+
},
|
|
154
|
+
context: {
|
|
155
|
+
formId: mutationContextId,
|
|
156
|
+
resourceKind: "currencies.fetch_config",
|
|
157
|
+
resourceId: configId,
|
|
158
|
+
retryLastMutation
|
|
159
|
+
},
|
|
160
|
+
mutationPayload: { id: configId, syncTime: syncTime || null }
|
|
161
|
+
});
|
|
162
|
+
const result = call.result;
|
|
110
163
|
if (result?.config) {
|
|
111
164
|
setConfigs(
|
|
112
165
|
(prev) => prev.map((c) => c.id === configId ? result.config : c)
|
|
113
166
|
);
|
|
114
167
|
}
|
|
115
168
|
} catch (err) {
|
|
169
|
+
if (extractOptimisticLockConflict(err)) return;
|
|
116
170
|
flash(err.message || t("currencies.fetch.error_update_sync_time"), "error");
|
|
117
171
|
}
|
|
118
|
-
}, [t]);
|
|
172
|
+
}, [mutationContextId, retryLastMutation, runMutation, t]);
|
|
119
173
|
const fetchNow = useCallback(async (provider) => {
|
|
120
174
|
setFetching(provider);
|
|
121
175
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
176
|
+
const call = await runMutation({
|
|
177
|
+
operation: () => apiCall("/api/currencies/fetch-rates", {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
providers: [provider]
|
|
182
|
+
})
|
|
183
|
+
}),
|
|
184
|
+
context: {
|
|
185
|
+
formId: mutationContextId,
|
|
186
|
+
resourceKind: "currencies.fetch_rates",
|
|
187
|
+
retryLastMutation
|
|
188
|
+
},
|
|
189
|
+
mutationPayload: { providers: [provider] }
|
|
128
190
|
});
|
|
191
|
+
const result = call.result;
|
|
129
192
|
if (result) {
|
|
130
193
|
const byProvider = result.byProvider;
|
|
131
194
|
const count = byProvider?.[provider]?.count || 0;
|
|
@@ -141,7 +204,7 @@ function CurrencyFetchingConfig() {
|
|
|
141
204
|
} finally {
|
|
142
205
|
setFetching(null);
|
|
143
206
|
}
|
|
144
|
-
}, [t, loadConfigs]);
|
|
207
|
+
}, [mutationContextId, retryLastMutation, runMutation, t, loadConfigs]);
|
|
145
208
|
useEffect(() => {
|
|
146
209
|
loadConfigs();
|
|
147
210
|
}, [loadConfigs]);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/currencies/components/CurrencyFetchingConfig.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport { useState, useEffect, useCallback, useMemo } from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface FetchConfig {\n id: string\n provider: string\n isEnabled: boolean\n syncTime: string | null\n lastSyncAt: string | null\n lastSyncStatus: string | null\n lastSyncMessage: string | null\n lastSyncCount: number | null\n updatedAt?: string | null\n}\n\nexport default function CurrencyFetchingConfig() {\n const t = useT()\n const [configs, setConfigs] = useState<FetchConfig[]>([])\n const [loading, setLoading] = useState(true)\n const [fetching, setFetching] = useState<string | null>(null)\n const [initializing, setInitializing] = useState(false)\n\n // Available providers that should be configured\n const availableProviders = useMemo(() => ['NBP', 'Raiffeisen Bank Polska'], [])\n\n const createProviderConfig = useCallback(async (provider: string) => {\n try {\n await apiCall('/api/currencies/fetch-configs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n provider,\n isEnabled: false,\n syncTime: '09:00',\n }),\n })\n } catch (err: any) {\n // Ignore errors for duplicate providers\n if (!err.message?.includes('already configured')) {\n throw err\n }\n }\n }, [])\n\n const initializeMissingProviders = useCallback(async (providers: string[]) => {\n setInitializing(true)\n try {\n for (const provider of providers) {\n await createProviderConfig(provider)\n }\n // Reload configs after initialization\n const { result } = await apiCall('/api/currencies/fetch-configs')\n if (result?.configs) {\n setConfigs(result.configs as FetchConfig[])\n }\n } catch (err: any) {\n console.error('Failed to initialize providers:', err)\n } finally {\n setInitializing(false)\n }\n }, [createProviderConfig])\n\n const loadConfigs = useCallback(async () => {\n try {\n const { result } = await apiCall('/api/currencies/fetch-configs')\n\n if (result?.configs) {\n const existingConfigs = result.configs as FetchConfig[]\n setConfigs(existingConfigs)\n\n // Check if we need to initialize any missing providers\n const existingProviders = existingConfigs.map((c) => c.provider)\n const missingProviders = availableProviders.filter(\n (p) => !existingProviders.includes(p)\n )\n\n // Auto-initialize missing providers\n if (missingProviders.length > 0) {\n await initializeMissingProviders(missingProviders)\n }\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_load_configs'), 'error')\n } finally {\n setLoading(false)\n }\n }, [availableProviders, initializeMissingProviders, t])\n\n const toggleEnabled = useCallback(async (configId: string, currentValue: boolean, updatedAt?: string | null) => {\n try {\n const { result } = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n isEnabled: !currentValue,\n }),\n }),\n )\n\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n flash(\n currentValue\n ? t('currencies.fetch.provider_disabled')\n : t('currencies.fetch.provider_enabled'),\n 'success'\n )\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_update_config'), 'error')\n }\n }, [t])\n\n const updateSyncTime = useCallback(async (configId: string, syncTime: string, updatedAt?: string | null) => {\n try {\n const { result } = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n syncTime: syncTime || null,\n }),\n }),\n )\n\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_update_sync_time'), 'error')\n }\n }, [t])\n\n const fetchNow = useCallback(async (provider: string) => {\n setFetching(provider)\n\n try {\n const { result } = await apiCall('/api/currencies/fetch-rates', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n providers: [provider],\n }),\n })\n\n if (result) {\n const byProvider = result.byProvider as Record<string, { count: number; errors?: string[] }>\n const count = byProvider?.[provider]?.count || 0\n\n if (count === 0) {\n flash(t('currencies.fetch.sync_no_rates'), 'warning')\n } else {\n flash(`${t('currencies.fetch.sync_success')}: ${count} rates fetched`, 'success')\n }\n await loadConfigs()\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.sync_error'), 'error')\n } finally {\n setFetching(null)\n }\n }, [t, loadConfigs])\n\n useEffect(() => {\n loadConfigs()\n }, [loadConfigs])\n\n function formatLastSync(date: string | null): string {\n if (!date) return 'Never'\n return new Date(date).toLocaleString()\n }\n\n function getStatusBadge(status: string | null) {\n if (!status) return null\n\n const styles: Record<string, string> = {\n success: 'bg-green-100 text-green-800 px-2 py-1 rounded text-xs',\n error: 'bg-red-100 text-red-800 px-2 py-1 rounded text-xs',\n partial: 'bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs',\n }\n\n return (\n <span className={styles[status] || styles.error}>\n {status}\n </span>\n )\n }\n\n function getProviderName(provider: string): string {\n if (provider === 'NBP') return t('currencies.fetch.provider_nbp')\n if (provider === 'Raiffeisen Bank Polska') return t('currencies.fetch.provider_raiffeisen')\n return provider\n }\n\n function getProviderDescription(provider: string): string {\n if (provider === 'NBP') {\n return t('currencies.fetch.provider_nbp_description')\n }\n if (provider === 'Raiffeisen Bank Polska') {\n return t('currencies.fetch.provider_raiffeisen_description')\n }\n return ''\n }\n\n if (loading || initializing) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {initializing ? t('currencies.fetch.initializing_providers') : t('currencies.fetch.loading')}\n </div>\n </section>\n )\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n\n <div className=\"space-y-3\">\n {configs.map((config) => (\n <div\n key={config.id}\n className=\"rounded-lg border bg-card p-4\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1\">\n <h3 className=\"text-sm font-medium\">\n {getProviderName(config.provider)}\n </h3>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {getProviderDescription(config.provider)}\n </p>\n </div>\n\n <Switch\n checked={config.isEnabled}\n onCheckedChange={() => toggleEnabled(config.id, config.isEnabled, config.updatedAt)}\n />\n </div>\n\n {config.isEnabled && (\n <>\n <div className=\"flex items-baseline gap-3 flex-wrap mt-3\">\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.sync_time')}:\n </label>\n <input\n type=\"time\"\n value={config.syncTime || '09:00'}\n onChange={(e) => updateSyncTime(config.id, e.target.value, config.updatedAt)}\n className=\"rounded border bg-background px-2 py-1.5 text-sm\"\n />\n </div>\n\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.last_sync')}:\n </label>\n <span className=\"text-xs text-muted-foreground\">\n {formatLastSync(config.lastSyncAt)}\n </span>\n {getStatusBadge(config.lastSyncStatus)}\n </div>\n\n <Button\n onClick={() => fetchNow(config.provider)}\n disabled={fetching === config.provider}\n size=\"default\"\n className=\"ml-auto min-w-32\"\n >\n {fetching === config.provider ? (\n <div>\n <Spinner className=\"mr-1.5\" size='sm' />\n {t('currencies.fetch.fetching')}\n </div>\n ) : (\n t('currencies.fetch.fetch_now')\n )}\n </Button>\n </div>\n\n {config.lastSyncCount !== null && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n {t('currencies.fetch.last_sync_count')}: <span className=\"font-medium\">{config.lastSyncCount}</span> rates\n </div>\n )}\n\n {config.lastSyncMessage && config.lastSyncStatus === 'error' && (\n <div className=\"rounded border border-red-200 bg-red-50 p-2 text-xs text-red-700 mt-2\">\n {config.lastSyncMessage}\n </div>\n )}\n </>\n )}\n </div>\n ))}\n\n {configs.length === 0 && (\n <div className=\"rounded border bg-background/80 p-6 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.no_providers')}\n </p>\n <Button\n onClick={() => initializeMissingProviders(availableProviders)}\n >\n {t('currencies.fetch.initialize_providers')}\n </Button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport { useState, useEffect, useCallback, useMemo } from 'react'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'\nimport { buildOptimisticLockHeader, extractOptimisticLockConflict } from '@open-mercato/ui/backend/utils/optimisticLock'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ninterface FetchConfig {\n id: string\n provider: string\n isEnabled: boolean\n syncTime: string | null\n lastSyncAt: string | null\n lastSyncStatus: string | null\n lastSyncMessage: string | null\n lastSyncCount: number | null\n updatedAt?: string | null\n}\n\nexport default function CurrencyFetchingConfig() {\n const t = useT()\n const [configs, setConfigs] = useState<FetchConfig[]>([])\n const [loading, setLoading] = useState(true)\n const [fetching, setFetching] = useState<string | null>(null)\n const [initializing, setInitializing] = useState(false)\n\n // Available providers that should be configured\n const availableProviders = useMemo(() => ['NBP', 'Raiffeisen Bank Polska'], [])\n\n const mutationContextId = 'currencies-fetch-configs:mutation'\n const { runMutation, retryLastMutation } = useGuardedMutation<{\n formId: string\n resourceKind: string\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n }>({\n contextId: mutationContextId,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const createProviderConfig = useCallback(async (provider: string) => {\n try {\n await runMutation({\n operation: () => apiCall('/api/currencies/fetch-configs', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n provider,\n isEnabled: false,\n syncTime: '09:00',\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n retryLastMutation,\n },\n mutationPayload: { provider, isEnabled: false, syncTime: '09:00' },\n })\n } catch (err: any) {\n // Ignore errors for duplicate providers\n if (!err.message?.includes('already configured')) {\n throw err\n }\n }\n }, [mutationContextId, retryLastMutation, runMutation])\n\n const initializeMissingProviders = useCallback(async (providers: string[]) => {\n setInitializing(true)\n try {\n for (const provider of providers) {\n await createProviderConfig(provider)\n }\n // Reload configs after initialization\n const { result } = await apiCall('/api/currencies/fetch-configs')\n if (result?.configs) {\n setConfigs(result.configs as FetchConfig[])\n }\n } catch (err: any) {\n console.error('Failed to initialize providers:', err)\n } finally {\n setInitializing(false)\n }\n }, [createProviderConfig])\n\n const loadConfigs = useCallback(async () => {\n try {\n const { result } = await apiCall('/api/currencies/fetch-configs')\n\n if (result?.configs) {\n const existingConfigs = result.configs as FetchConfig[]\n setConfigs(existingConfigs)\n\n // Check if we need to initialize any missing providers\n const existingProviders = existingConfigs.map((c) => c.provider)\n const missingProviders = availableProviders.filter(\n (p) => !existingProviders.includes(p)\n )\n\n // Auto-initialize missing providers\n if (missingProviders.length > 0) {\n await initializeMissingProviders(missingProviders)\n }\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.error_load_configs'), 'error')\n } finally {\n setLoading(false)\n }\n }, [availableProviders, initializeMissingProviders, t])\n\n const toggleEnabled = useCallback(async (configId: string, currentValue: boolean, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n isEnabled: !currentValue,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.toggle failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, isEnabled: !currentValue },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n flash(\n currentValue\n ? t('currencies.fetch.provider_disabled')\n : t('currencies.fetch.provider_enabled'),\n 'success'\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_config'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const updateSyncTime = useCallback(async (configId: string, syncTime: string, updatedAt?: string | null) => {\n try {\n const call = await runMutation({\n operation: async () => {\n const response = await withScopedApiRequestHeaders(\n buildOptimisticLockHeader(updatedAt),\n () =>\n apiCall('/api/currencies/fetch-configs', {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n id: configId,\n syncTime: syncTime || null,\n }),\n }),\n )\n if (!response.ok) {\n throw Object.assign(new Error('[internal] currencies.fetchConfig.updateSyncTime failed'), {\n status: response.status,\n ...((response.result as Record<string, unknown> | null) ?? {}),\n })\n }\n return response\n },\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_config',\n resourceId: configId,\n retryLastMutation,\n },\n mutationPayload: { id: configId, syncTime: syncTime || null },\n })\n\n const result = call.result as { config?: FetchConfig } | null\n if (result?.config) {\n setConfigs((prev) =>\n prev.map((c) => (c.id === configId ? (result.config as FetchConfig) : c))\n )\n }\n } catch (err: any) {\n if (extractOptimisticLockConflict(err)) return\n flash(err.message || t('currencies.fetch.error_update_sync_time'), 'error')\n }\n }, [mutationContextId, retryLastMutation, runMutation, t])\n\n const fetchNow = useCallback(async (provider: string) => {\n setFetching(provider)\n\n try {\n const call = await runMutation({\n operation: () => apiCall('/api/currencies/fetch-rates', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n providers: [provider],\n }),\n }),\n context: {\n formId: mutationContextId,\n resourceKind: 'currencies.fetch_rates',\n retryLastMutation,\n },\n mutationPayload: { providers: [provider] },\n })\n\n const result = call.result as { byProvider?: Record<string, { count: number; errors?: string[] }> } | null\n if (result) {\n const byProvider = result.byProvider as Record<string, { count: number; errors?: string[] }>\n const count = byProvider?.[provider]?.count || 0\n\n if (count === 0) {\n flash(t('currencies.fetch.sync_no_rates'), 'warning')\n } else {\n flash(`${t('currencies.fetch.sync_success')}: ${count} rates fetched`, 'success')\n }\n await loadConfigs()\n }\n } catch (err: any) {\n flash(err.message || t('currencies.fetch.sync_error'), 'error')\n } finally {\n setFetching(null)\n }\n }, [mutationContextId, retryLastMutation, runMutation, t, loadConfigs])\n\n useEffect(() => {\n loadConfigs()\n }, [loadConfigs])\n\n function formatLastSync(date: string | null): string {\n if (!date) return 'Never'\n return new Date(date).toLocaleString()\n }\n\n function getStatusBadge(status: string | null) {\n if (!status) return null\n\n const styles: Record<string, string> = {\n success: 'bg-green-100 text-green-800 px-2 py-1 rounded text-xs',\n error: 'bg-red-100 text-red-800 px-2 py-1 rounded text-xs',\n partial: 'bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs',\n }\n\n return (\n <span className={styles[status] || styles.error}>\n {status}\n </span>\n )\n }\n\n function getProviderName(provider: string): string {\n if (provider === 'NBP') return t('currencies.fetch.provider_nbp')\n if (provider === 'Raiffeisen Bank Polska') return t('currencies.fetch.provider_raiffeisen')\n return provider\n }\n\n function getProviderDescription(provider: string): string {\n if (provider === 'NBP') {\n return t('currencies.fetch.provider_nbp_description')\n }\n if (provider === 'Raiffeisen Bank Polska') {\n return t('currencies.fetch.provider_raiffeisen_description')\n }\n return ''\n }\n\n if (loading || initializing) {\n return (\n <section className=\"space-y-3 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n <div className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n {initializing ? t('currencies.fetch.initializing_providers') : t('currencies.fetch.loading')}\n </div>\n </section>\n )\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-4\">\n <header className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold\">{t('currencies.fetch.title')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.description')}\n </p>\n </header>\n\n <div className=\"space-y-3\">\n {configs.map((config) => (\n <div\n key={config.id}\n className=\"rounded-lg border bg-card p-4\"\n >\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1\">\n <h3 className=\"text-sm font-medium\">\n {getProviderName(config.provider)}\n </h3>\n <p className=\"text-xs text-muted-foreground mt-0.5\">\n {getProviderDescription(config.provider)}\n </p>\n </div>\n\n <Switch\n checked={config.isEnabled}\n onCheckedChange={() => toggleEnabled(config.id, config.isEnabled, config.updatedAt)}\n />\n </div>\n\n {config.isEnabled && (\n <>\n <div className=\"flex items-baseline gap-3 flex-wrap mt-3\">\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.sync_time')}:\n </label>\n <input\n type=\"time\"\n value={config.syncTime || '09:00'}\n onChange={(e) => updateSyncTime(config.id, e.target.value, config.updatedAt)}\n className=\"rounded border bg-background px-2 py-1.5 text-sm\"\n />\n </div>\n\n <div className=\"flex items-baseline gap-2\">\n <label className=\"text-xs text-muted-foreground whitespace-nowrap\">\n {t('currencies.fetch.last_sync')}:\n </label>\n <span className=\"text-xs text-muted-foreground\">\n {formatLastSync(config.lastSyncAt)}\n </span>\n {getStatusBadge(config.lastSyncStatus)}\n </div>\n\n <Button\n onClick={() => fetchNow(config.provider)}\n disabled={fetching === config.provider}\n size=\"default\"\n className=\"ml-auto min-w-32\"\n >\n {fetching === config.provider ? (\n <div>\n <Spinner className=\"mr-1.5\" size='sm' />\n {t('currencies.fetch.fetching')}\n </div>\n ) : (\n t('currencies.fetch.fetch_now')\n )}\n </Button>\n </div>\n\n {config.lastSyncCount !== null && (\n <div className=\"text-xs text-muted-foreground mt-2\">\n {t('currencies.fetch.last_sync_count')}: <span className=\"font-medium\">{config.lastSyncCount}</span> rates\n </div>\n )}\n\n {config.lastSyncMessage && config.lastSyncStatus === 'error' && (\n <div className=\"rounded border border-red-200 bg-red-50 p-2 text-xs text-red-700 mt-2\">\n {config.lastSyncMessage}\n </div>\n )}\n </>\n )}\n </div>\n ))}\n\n {configs.length === 0 && (\n <div className=\"rounded border bg-background/80 p-6 text-center\">\n <p className=\"text-sm text-muted-foreground\">\n {t('currencies.fetch.no_providers')}\n </p>\n <Button\n onClick={() => initializeMissingProviders(availableProviders)}\n >\n {t('currencies.fetch.initialize_providers')}\n </Button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,15 +4,23 @@ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
|
|
|
4
4
|
import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
|
|
5
5
|
import { CustomerRole, CustomerRoleAcl } from "@open-mercato/core/modules/customer_accounts/data/entities";
|
|
6
6
|
import { updateRoleAclSchema } from "@open-mercato/core/modules/customer_accounts/data/validators";
|
|
7
|
+
import { enforceCommandOptimisticLock } from "@open-mercato/shared/lib/crud/optimistic-lock-command";
|
|
8
|
+
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
|
+
import {
|
|
10
|
+
runCrudMutationGuardAfterSuccess,
|
|
11
|
+
validateCrudMutationGuard
|
|
12
|
+
} from "@open-mercato/shared/lib/crud/mutation-guard";
|
|
7
13
|
const metadata = {};
|
|
14
|
+
const ROLE_RESOURCE_KIND = "customer_accounts.role";
|
|
8
15
|
async function PUT(req, { params }) {
|
|
9
16
|
const auth = await getAuthFromRequest(req);
|
|
10
|
-
if (!auth) {
|
|
17
|
+
if (!auth || !auth.tenantId) {
|
|
11
18
|
return NextResponse.json({ ok: false, error: "Authentication required" }, { status: 401 });
|
|
12
19
|
}
|
|
20
|
+
const tenantId = auth.tenantId;
|
|
13
21
|
const container = await createRequestContainer();
|
|
14
22
|
const rbacService = container.resolve("rbacService");
|
|
15
|
-
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ["customer_accounts.roles.manage"], { tenantId
|
|
23
|
+
const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ["customer_accounts.roles.manage"], { tenantId, organizationId: auth.orgId });
|
|
16
24
|
if (!hasAccess) {
|
|
17
25
|
return NextResponse.json({ ok: false, error: "Insufficient permissions" }, { status: 403 });
|
|
18
26
|
}
|
|
@@ -29,15 +37,40 @@ async function PUT(req, { params }) {
|
|
|
29
37
|
const em = container.resolve("em");
|
|
30
38
|
const role = await em.findOne(CustomerRole, {
|
|
31
39
|
id: params.id,
|
|
32
|
-
tenantId
|
|
40
|
+
tenantId,
|
|
33
41
|
deletedAt: null
|
|
34
42
|
});
|
|
35
43
|
if (!role) {
|
|
36
44
|
return NextResponse.json({ ok: false, error: "Role not found" }, { status: 404 });
|
|
37
45
|
}
|
|
46
|
+
try {
|
|
47
|
+
enforceCommandOptimisticLock({
|
|
48
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
49
|
+
resourceId: role.id,
|
|
50
|
+
current: role.updatedAt ?? null,
|
|
51
|
+
request: req
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status });
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
58
|
+
tenantId,
|
|
59
|
+
organizationId: auth.orgId,
|
|
60
|
+
userId: auth.sub,
|
|
61
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
62
|
+
resourceId: role.id,
|
|
63
|
+
operation: "update",
|
|
64
|
+
requestMethod: req.method,
|
|
65
|
+
requestHeaders: req.headers,
|
|
66
|
+
mutationPayload: parsed.data
|
|
67
|
+
});
|
|
68
|
+
if (guardResult && !guardResult.ok) {
|
|
69
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status });
|
|
70
|
+
}
|
|
38
71
|
const acl = await em.findOne(CustomerRoleAcl, {
|
|
39
72
|
role: role.id,
|
|
40
|
-
tenantId
|
|
73
|
+
tenantId
|
|
41
74
|
});
|
|
42
75
|
if (acl) {
|
|
43
76
|
await em.nativeUpdate(CustomerRoleAcl, { id: acl.id }, {
|
|
@@ -47,7 +80,7 @@ async function PUT(req, { params }) {
|
|
|
47
80
|
} else {
|
|
48
81
|
const newAcl = em.create(CustomerRoleAcl, {
|
|
49
82
|
role,
|
|
50
|
-
tenantId
|
|
83
|
+
tenantId,
|
|
51
84
|
featuresJson: parsed.data.features,
|
|
52
85
|
isPortalAdmin: parsed.data.isPortalAdmin ?? false,
|
|
53
86
|
createdAt: /* @__PURE__ */ new Date()
|
|
@@ -55,11 +88,26 @@ async function PUT(req, { params }) {
|
|
|
55
88
|
em.persist(newAcl);
|
|
56
89
|
await em.flush();
|
|
57
90
|
}
|
|
91
|
+
const nextUpdatedAt = /* @__PURE__ */ new Date();
|
|
92
|
+
await em.nativeUpdate(CustomerRole, { id: role.id }, { updatedAt: nextUpdatedAt });
|
|
58
93
|
const customerRbacService = container.resolve("customerRbacService");
|
|
59
94
|
await customerRbacService.invalidateRoleCache(role.id);
|
|
60
|
-
|
|
95
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
96
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
97
|
+
tenantId,
|
|
98
|
+
organizationId: auth.orgId,
|
|
99
|
+
userId: auth.sub,
|
|
100
|
+
resourceKind: ROLE_RESOURCE_KIND,
|
|
101
|
+
resourceId: role.id,
|
|
102
|
+
operation: "update",
|
|
103
|
+
requestMethod: req.method,
|
|
104
|
+
requestHeaders: req.headers,
|
|
105
|
+
metadata: guardResult.metadata ?? null
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return NextResponse.json({ ok: true, updatedAt: nextUpdatedAt.toISOString() });
|
|
61
109
|
}
|
|
62
|
-
const successSchema = z.object({ ok: z.literal(true) });
|
|
110
|
+
const successSchema = z.object({ ok: z.literal(true), updatedAt: z.string().datetime() });
|
|
63
111
|
const errorSchema = z.object({ ok: z.literal(false), error: z.string() });
|
|
64
112
|
const methodDoc = {
|
|
65
113
|
summary: "Update customer role ACL (admin)",
|
|
@@ -71,7 +119,8 @@ const methodDoc = {
|
|
|
71
119
|
{ status: 400, description: "Validation failed", schema: errorSchema },
|
|
72
120
|
{ status: 401, description: "Not authenticated", schema: errorSchema },
|
|
73
121
|
{ status: 403, description: "Insufficient permissions", schema: errorSchema },
|
|
74
|
-
{ status: 404, description: "Role not found", schema: errorSchema }
|
|
122
|
+
{ status: 404, description: "Role not found", schema: errorSchema },
|
|
123
|
+
{ status: 409, description: "Stale ACL write (optimistic-lock conflict)", schema: errorSchema }
|
|
75
124
|
]
|
|
76
125
|
};
|
|
77
126
|
const openApi = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/customer_accounts/api/admin/roles/%5Bid%5D/acl.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { updateRoleAclSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\n\nexport const metadata = {}\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,cAAc,uBAAuB;AAE9C,SAAS,2BAA2B;
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { CustomerRbacService } from '@open-mercato/core/modules/customer_accounts/services/customerRbacService'\nimport { updateRoleAclSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\n\nexport const metadata = {}\n\nconst ROLE_RESOURCE_KIND = 'customer_accounts.role'\n\nexport async function PUT(req: Request, { params }: { params: { id: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId) {\n return NextResponse.json({ ok: false, error: 'Authentication required' }, { status: 401 })\n }\n const tenantId = auth.tenantId\n\n const container = await createRequestContainer()\n const rbacService = container.resolve('rbacService') as RbacService\n const hasAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.roles.manage'], { tenantId, organizationId: auth.orgId })\n if (!hasAccess) {\n return NextResponse.json({ ok: false, error: 'Insufficient permissions' }, { status: 403 })\n }\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = updateRoleAclSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n\n const role = await em.findOne(CustomerRole, {\n id: params.id,\n tenantId,\n deletedAt: null,\n })\n if (!role) {\n return NextResponse.json({ ok: false, error: 'Role not found' }, { status: 404 })\n }\n\n // The ACL is part of the role aggregate: the role-edit screen loads and saves\n // the role and its permissions together. Guard the write against the parent\n // role's `updatedAt` so two admins editing the same role cannot silently\n // overwrite each other's permission changes (#3194). Strictly additive \u2014 when\n // the client sends no expected-version header the helper is a no-op.\n try {\n enforceCommandOptimisticLock({\n resourceKind: ROLE_RESOURCE_KIND,\n resourceId: role.id,\n current: role.updatedAt ?? null,\n request: req,\n })\n } catch (err) {\n if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })\n throw err\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n resourceKind: ROLE_RESOURCE_KIND,\n resourceId: role.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const acl = await em.findOne(CustomerRoleAcl, {\n role: role.id as any,\n tenantId,\n })\n\n if (acl) {\n await em.nativeUpdate(CustomerRoleAcl, { id: acl.id }, {\n featuresJson: parsed.data.features,\n isPortalAdmin: parsed.data.isPortalAdmin ?? acl.isPortalAdmin,\n })\n } else {\n const newAcl = em.create(CustomerRoleAcl, {\n role,\n tenantId,\n featuresJson: parsed.data.features,\n isPortalAdmin: parsed.data.isPortalAdmin ?? false,\n createdAt: new Date(),\n } as any)\n em.persist(newAcl)\n await em.flush()\n }\n\n // Bump the aggregate version so a stale role-edit screen (which holds the role's\n // pre-edit `updatedAt`) cannot save old permission arrays afterwards. `nativeUpdate`\n // bypasses MikroORM's `onUpdate` hook, so set it explicitly.\n const nextUpdatedAt = new Date()\n await em.nativeUpdate(CustomerRole, { id: role.id }, { updatedAt: nextUpdatedAt })\n\n const customerRbacService = container.resolve('customerRbacService') as CustomerRbacService\n await customerRbacService.invalidateRoleCache(role.id)\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId,\n organizationId: auth.orgId,\n userId: auth.sub,\n resourceKind: ROLE_RESOURCE_KIND,\n resourceId: role.id,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ ok: true, updatedAt: nextUpdatedAt.toISOString() })\n}\n\nconst successSchema = z.object({ ok: z.literal(true), updatedAt: z.string().datetime() })\nconst errorSchema = z.object({ ok: z.literal(false), error: z.string() })\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Update customer role ACL (admin)',\n description: 'Updates the ACL (features and portal admin flag) for a customer role. Invalidates RBAC cache after update.',\n tags: ['Customer Accounts Admin'],\n requestBody: { schema: updateRoleAclSchema },\n responses: [{ status: 200, description: 'ACL updated', schema: successSchema }],\n errors: [\n { status: 400, description: 'Validation failed', schema: errorSchema },\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'Insufficient permissions', schema: errorSchema },\n { status: 404, description: 'Role not found', schema: errorSchema },\n { status: 409, description: 'Stale ACL write (optimistic-lock conflict)', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Update customer role ACL (admin)',\n pathParams: z.object({ id: z.string().uuid() }),\n methods: { PUT: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -241,15 +241,18 @@ function CustomerRoleDetailPage({ params }) {
|
|
|
241
241
|
return;
|
|
242
242
|
}
|
|
243
243
|
const features = Array.isArray(values.features) ? values.features : [];
|
|
244
|
-
const
|
|
244
|
+
const roleResult = roleCall.result ?? null;
|
|
245
|
+
const aclHeaders = buildOptimisticLockHeader(roleResult?.updatedAt ?? data?.updatedAt ?? data?.updated_at ?? null);
|
|
246
|
+
const aclCall = await withScopedApiRequestHeaders(aclHeaders, () => apiCall(
|
|
245
247
|
`/api/customer_accounts/admin/roles/${encodeURIComponent(id)}/acl`,
|
|
246
248
|
{
|
|
247
249
|
method: "PUT",
|
|
248
250
|
headers: { "content-type": "application/json" },
|
|
249
251
|
body: JSON.stringify({ features })
|
|
250
252
|
}
|
|
251
|
-
);
|
|
253
|
+
));
|
|
252
254
|
if (!aclCall.ok) {
|
|
255
|
+
if (surfaceRecordConflict({ status: aclCall.status, body: aclCall.result }, t)) return;
|
|
253
256
|
flash(t("customer_accounts.admin.roleDetail.error.saveAcl", "Failed to save permissions"), "error");
|
|
254
257
|
return;
|
|
255
258
|
}
|