@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6241.1.47c01ad4ed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/commands/users.js +1 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +9 -1
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js +3 -5
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.js.map +2 -2
- package/dist/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +39 -1
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/route.js +107 -12
- package/dist/modules/customers/api/pipeline-stages/route.js.map +2 -2
- package/dist/modules/customers/api/pipelines/route.js +107 -12
- package/dist/modules/customers/api/pipelines/route.js.map +2 -2
- package/dist/modules/customers/api/settings/address-format/route.js +33 -1
- package/dist/modules/customers/api/settings/address-format/route.js.map +2 -2
- package/dist/modules/customers/api/tags/assign/route.js +37 -0
- package/dist/modules/customers/api/tags/assign/route.js.map +2 -2
- package/dist/modules/customers/api/tags/unassign/route.js +37 -0
- package/dist/modules/customers/api/tags/unassign/route.js.map +2 -2
- package/dist/modules/entities/api/encryption.js +109 -38
- package/dist/modules/entities/api/encryption.js.map +2 -2
- package/dist/modules/entities/components/EncryptionManager.js +22 -9
- package/dist/modules/entities/components/EncryptionManager.js.map +2 -2
- package/dist/modules/integrations/api/[id]/credentials/route.js +23 -1
- package/dist/modules/integrations/api/[id]/credentials/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/route.js +10 -3
- package/dist/modules/integrations/api/[id]/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/state/route.js +20 -5
- package/dist/modules/integrations/api/[id]/state/route.js.map +2 -2
- package/dist/modules/integrations/api/[id]/version/route.js +17 -1
- package/dist/modules/integrations/api/[id]/version/route.js.map +2 -2
- package/dist/modules/integrations/api/route.js +2 -0
- package/dist/modules/integrations/api/route.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +36 -19
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/page.js +4 -3
- package/dist/modules/integrations/backend/integrations/page.js.map +2 -2
- package/dist/modules/integrations/data/entities.js +2 -2
- package/dist/modules/integrations/data/entities.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +32 -0
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/integrations/lib/state-service.js +4 -2
- package/dist/modules/integrations/lib/state-service.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +42 -10
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/payment_gateways/api/status/route.js +91 -6
- package/dist/modules/payment_gateways/api/status/route.js.map +2 -2
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js +25 -11
- package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/offers/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/offers/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/channels/page.js +25 -7
- package/dist/modules/sales/backend/sales/channels/page.js.map +2 -2
- package/dist/modules/sales/components/AdjustmentKindSettings.js +52 -24
- package/dist/modules/sales/components/AdjustmentKindSettings.js.map +2 -2
- package/dist/modules/sales/components/DocumentNumberSettings.js +26 -9
- package/dist/modules/sales/components/DocumentNumberSettings.js.map +2 -2
- package/dist/modules/sales/components/OrderEditingSettings.js +26 -9
- package/dist/modules/sales/components/OrderEditingSettings.js.map +2 -2
- package/dist/modules/sales/components/StatusSettings.js +66 -31
- package/dist/modules/sales/components/StatusSettings.js.map +2 -2
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js +25 -7
- package/dist/modules/sales/components/channels/SalesChannelOffersPanel.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +38 -0
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/shipments/route.js +12 -5
- package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js +7 -8
- package/dist/modules/staff/lib/timesheets-ui/TimerBar.js.map +2 -2
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js +12 -0
- package/dist/modules/staff/lib/timesheets-ui/timerErrors.js.map +7 -0
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js +4 -3
- package/dist/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +19 -10
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/commands/users.ts +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +25 -0
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.client.tsx +7 -6
- package/src/modules/communication_channels/widgets/injection/channel-payload-renderer/widget.ts +5 -2
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +41 -1
- package/src/modules/customers/api/pipeline-stages/route.ts +112 -13
- package/src/modules/customers/api/pipelines/route.ts +112 -13
- package/src/modules/customers/api/settings/address-format/route.ts +36 -1
- package/src/modules/customers/api/tags/assign/route.ts +39 -0
- package/src/modules/customers/api/tags/unassign/route.ts +39 -0
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/entities/api/encryption.ts +122 -39
- package/src/modules/entities/components/EncryptionManager.tsx +28 -9
- package/src/modules/integrations/api/[id]/credentials/route.ts +23 -0
- package/src/modules/integrations/api/[id]/route.ts +8 -1
- package/src/modules/integrations/api/[id]/state/route.ts +20 -4
- package/src/modules/integrations/api/[id]/version/route.ts +18 -1
- package/src/modules/integrations/api/route.ts +3 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +39 -20
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +19 -11
- package/src/modules/integrations/backend/integrations/page.tsx +8 -5
- package/src/modules/integrations/data/entities.ts +2 -2
- package/src/modules/integrations/lib/credentials-service.ts +35 -0
- package/src/modules/integrations/lib/state-service.ts +3 -0
- package/src/modules/notifications/lib/notificationService.ts +74 -11
- package/src/modules/payment_gateways/api/status/route.ts +97 -5
- package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +27 -11
- package/src/modules/sales/backend/sales/channels/offers/page.tsx +31 -7
- package/src/modules/sales/backend/sales/channels/page.tsx +31 -7
- package/src/modules/sales/components/AdjustmentKindSettings.tsx +60 -24
- package/src/modules/sales/components/DocumentNumberSettings.tsx +32 -10
- package/src/modules/sales/components/OrderEditingSettings.tsx +32 -10
- package/src/modules/sales/components/StatusSettings.tsx +74 -33
- package/src/modules/sales/components/channels/SalesChannelOffersPanel.tsx +30 -6
- package/src/modules/shipping_carriers/api/cancel/route.ts +46 -0
- package/src/modules/shipping_carriers/api/shipments/route.ts +21 -6
- package/src/modules/staff/i18n/de.json +5 -5
- package/src/modules/staff/i18n/es.json +5 -5
- package/src/modules/staff/i18n/pl.json +5 -5
- package/src/modules/staff/lib/timesheets-ui/TimerBar.tsx +7 -8
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +4 -3
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -13,6 +13,7 @@ import { readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato
|
|
|
13
13
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
14
14
|
import { deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
15
15
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
16
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
16
17
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
17
18
|
import { mapOfferRow, renderOfferPriceSummary, type OfferRow } from './offerTableUtils'
|
|
18
19
|
|
|
@@ -24,9 +25,27 @@ type OffersResponse = {
|
|
|
24
25
|
|
|
25
26
|
const PAGE_SIZE = 25
|
|
26
27
|
|
|
28
|
+
const SAVE_CONTEXT_ID = 'sales-channel-offers-panel'
|
|
29
|
+
|
|
27
30
|
export function SalesChannelOffersPanel({ channelId, channelName }: { channelId: string; channelName?: string }) {
|
|
28
31
|
const t = useT()
|
|
29
32
|
const router = useRouter()
|
|
33
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
34
|
+
formId: string
|
|
35
|
+
resourceKind: string
|
|
36
|
+
retryLastMutation: () => Promise<boolean>
|
|
37
|
+
}>({
|
|
38
|
+
contextId: SAVE_CONTEXT_ID,
|
|
39
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
40
|
+
})
|
|
41
|
+
const mutationContext = React.useMemo(
|
|
42
|
+
() => ({
|
|
43
|
+
formId: SAVE_CONTEXT_ID,
|
|
44
|
+
resourceKind: 'sales.channels.offers',
|
|
45
|
+
retryLastMutation,
|
|
46
|
+
}),
|
|
47
|
+
[retryLastMutation],
|
|
48
|
+
)
|
|
30
49
|
const [rows, setRows] = React.useState<OfferRow[]>([])
|
|
31
50
|
const [page, setPage] = React.useState(1)
|
|
32
51
|
const [total, setTotal] = React.useState(0)
|
|
@@ -125,11 +144,16 @@ export function SalesChannelOffersPanel({ channelId, channelName }: { channelId:
|
|
|
125
144
|
|
|
126
145
|
const handleDelete = React.useCallback(async (row: OfferRow) => {
|
|
127
146
|
try {
|
|
128
|
-
await
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
await runMutation({
|
|
148
|
+
operation: () =>
|
|
149
|
+
withScopedApiRequestHeaders(buildOptimisticLockHeader(row.updatedAt), () =>
|
|
150
|
+
deleteCrud('catalog/offers', row.id, {
|
|
151
|
+
errorMessage: t('sales.channels.offers.errors.delete', 'Failed to delete offer.'),
|
|
152
|
+
}),
|
|
153
|
+
),
|
|
154
|
+
context: mutationContext,
|
|
155
|
+
mutationPayload: { action: 'delete', id: row.id },
|
|
156
|
+
})
|
|
133
157
|
flash(t('sales.channels.offers.messages.deleted', 'Offer deleted.'), 'success')
|
|
134
158
|
setReloadToken((token) => token + 1)
|
|
135
159
|
} catch (err) {
|
|
@@ -137,7 +161,7 @@ export function SalesChannelOffersPanel({ channelId, channelName }: { channelId:
|
|
|
137
161
|
console.error('sales.channels.offers.delete', err)
|
|
138
162
|
flash(t('sales.channels.offers.errors.delete', 'Failed to delete offer.'), 'error')
|
|
139
163
|
}
|
|
140
|
-
}, [t])
|
|
164
|
+
}, [mutationContext, runMutation, t])
|
|
141
165
|
|
|
142
166
|
const handleSearchChange = React.useCallback((value: string) => {
|
|
143
167
|
setSearch(value)
|
|
@@ -2,11 +2,26 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
3
3
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
4
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
5
|
+
import {
|
|
6
|
+
runCrudMutationGuardAfterSuccess,
|
|
7
|
+
validateCrudMutationGuard,
|
|
8
|
+
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
5
9
|
import type { ShippingCarrierService } from '../../lib/shipping-service'
|
|
6
10
|
import { isShipmentCancelNotAllowedError } from '../../lib/status-sync'
|
|
7
11
|
import { cancelShipmentSchema } from '../../data/validators'
|
|
8
12
|
import { shippingCarriersTag } from '../openapi'
|
|
9
13
|
|
|
14
|
+
function resolveGuardUserId(auth: {
|
|
15
|
+
sub?: string | null
|
|
16
|
+
userId?: string | null
|
|
17
|
+
keyId?: string | null
|
|
18
|
+
}): string {
|
|
19
|
+
if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub
|
|
20
|
+
if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId
|
|
21
|
+
if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId
|
|
22
|
+
return 'system'
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
export const metadata = {
|
|
11
26
|
path: '/shipping-carriers/cancel',
|
|
12
27
|
POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },
|
|
@@ -23,6 +38,22 @@ export async function POST(req: Request) {
|
|
|
23
38
|
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
|
|
24
39
|
}
|
|
25
40
|
const container = await createRequestContainer()
|
|
41
|
+
const guardUserId = resolveGuardUserId(auth)
|
|
42
|
+
const guardResult = await validateCrudMutationGuard(container, {
|
|
43
|
+
tenantId: auth.tenantId,
|
|
44
|
+
organizationId: auth.orgId,
|
|
45
|
+
userId: guardUserId,
|
|
46
|
+
resourceKind: 'shipping_carriers.shipment',
|
|
47
|
+
resourceId: parsed.data.shipmentId,
|
|
48
|
+
operation: 'custom',
|
|
49
|
+
requestMethod: req.method,
|
|
50
|
+
requestHeaders: req.headers,
|
|
51
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
52
|
+
})
|
|
53
|
+
if (guardResult && !guardResult.ok) {
|
|
54
|
+
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
55
|
+
}
|
|
56
|
+
|
|
26
57
|
const service = container.resolve('shippingCarrierService') as ShippingCarrierService
|
|
27
58
|
try {
|
|
28
59
|
const result = await service.cancelShipment({
|
|
@@ -30,6 +61,21 @@ export async function POST(req: Request) {
|
|
|
30
61
|
organizationId: auth.orgId as string,
|
|
31
62
|
tenantId: auth.tenantId,
|
|
32
63
|
})
|
|
64
|
+
|
|
65
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
66
|
+
await runCrudMutationGuardAfterSuccess(container, {
|
|
67
|
+
tenantId: auth.tenantId,
|
|
68
|
+
organizationId: auth.orgId,
|
|
69
|
+
userId: guardUserId,
|
|
70
|
+
resourceKind: 'shipping_carriers.shipment',
|
|
71
|
+
resourceId: parsed.data.shipmentId,
|
|
72
|
+
operation: 'custom',
|
|
73
|
+
requestMethod: req.method,
|
|
74
|
+
requestHeaders: req.headers,
|
|
75
|
+
metadata: guardResult.metadata ?? null,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
return NextResponse.json(result)
|
|
34
80
|
} catch (error: unknown) {
|
|
35
81
|
if (isShipmentCancelNotAllowedError(error)) {
|
|
@@ -10,6 +10,17 @@ import type { ShippingCarrierService } from '../../lib/shipping-service'
|
|
|
10
10
|
import { createShipmentSchema } from '../../data/validators'
|
|
11
11
|
import { shippingCarriersTag } from '../openapi'
|
|
12
12
|
|
|
13
|
+
function resolveGuardUserId(auth: {
|
|
14
|
+
sub?: string | null
|
|
15
|
+
userId?: string | null
|
|
16
|
+
keyId?: string | null
|
|
17
|
+
}): string {
|
|
18
|
+
if (typeof auth.sub === 'string' && auth.sub.trim().length > 0) return auth.sub
|
|
19
|
+
if (typeof auth.userId === 'string' && auth.userId.trim().length > 0) return auth.userId
|
|
20
|
+
if (typeof auth.keyId === 'string' && auth.keyId.trim().length > 0) return auth.keyId
|
|
21
|
+
return 'system'
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export const metadata = {
|
|
14
25
|
path: '/shipping-carriers/shipments',
|
|
15
26
|
POST: { requireAuth: true, requireFeatures: ['shipping_carriers.manage'] },
|
|
@@ -26,40 +37,44 @@ export async function POST(req: Request) {
|
|
|
26
37
|
return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
|
|
27
38
|
}
|
|
28
39
|
const container = await createRequestContainer()
|
|
29
|
-
const
|
|
40
|
+
const guardUserId = resolveGuardUserId(auth)
|
|
30
41
|
const guardResult = await validateCrudMutationGuard(container, {
|
|
31
42
|
tenantId: auth.tenantId,
|
|
32
43
|
organizationId: auth.orgId,
|
|
33
|
-
userId:
|
|
44
|
+
userId: guardUserId,
|
|
34
45
|
resourceKind: 'shipping_carriers.shipment',
|
|
35
46
|
resourceId: parsed.data.orderId,
|
|
36
47
|
operation: 'create',
|
|
37
48
|
requestMethod: req.method,
|
|
38
49
|
requestHeaders: req.headers,
|
|
39
|
-
mutationPayload: parsed.data as
|
|
50
|
+
mutationPayload: parsed.data as Record<string, unknown>,
|
|
40
51
|
})
|
|
41
52
|
if (guardResult && !guardResult.ok) {
|
|
42
53
|
return NextResponse.json(guardResult.body, { status: guardResult.status })
|
|
43
54
|
}
|
|
55
|
+
|
|
56
|
+
const service = container.resolve('shippingCarrierService') as ShippingCarrierService
|
|
44
57
|
try {
|
|
45
58
|
const shipment = await service.createShipment({
|
|
46
59
|
...parsed.data,
|
|
47
60
|
organizationId: auth.orgId as string,
|
|
48
61
|
tenantId: auth.tenantId,
|
|
49
62
|
})
|
|
50
|
-
|
|
63
|
+
|
|
64
|
+
if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
|
|
51
65
|
await runCrudMutationGuardAfterSuccess(container, {
|
|
52
66
|
tenantId: auth.tenantId,
|
|
53
67
|
organizationId: auth.orgId,
|
|
54
|
-
userId:
|
|
68
|
+
userId: guardUserId,
|
|
55
69
|
resourceKind: 'shipping_carriers.shipment',
|
|
56
|
-
resourceId:
|
|
70
|
+
resourceId: parsed.data.orderId,
|
|
57
71
|
operation: 'create',
|
|
58
72
|
requestMethod: req.method,
|
|
59
73
|
requestHeaders: req.headers,
|
|
60
74
|
metadata: guardResult.metadata ?? null,
|
|
61
75
|
})
|
|
62
76
|
}
|
|
77
|
+
|
|
63
78
|
return NextResponse.json({
|
|
64
79
|
shipmentId: shipment.id,
|
|
65
80
|
carrierShipmentId: shipment.carrierShipmentId,
|
|
@@ -1002,9 +1002,9 @@
|
|
|
1002
1002
|
"staff.timesheets.my.timer.searchProject": "Search projects...",
|
|
1003
1003
|
"staff.timesheets.my.timer.selectProject": "Project",
|
|
1004
1004
|
"staff.timesheets.my.timer.start": "Start timer",
|
|
1005
|
-
"staff.timesheets.my.timer.startError": "
|
|
1005
|
+
"staff.timesheets.my.timer.startError": "Timer konnte nicht gestartet werden",
|
|
1006
1006
|
"staff.timesheets.my.timer.stop": "Stop timer",
|
|
1007
|
-
"staff.timesheets.my.timer.stopError": "
|
|
1007
|
+
"staff.timesheets.my.timer.stopError": "Timer konnte nicht gestoppt werden",
|
|
1008
1008
|
"staff.timesheets.my.timerStart": "Start Timer",
|
|
1009
1009
|
"staff.timesheets.my.timerStop": "Stop Timer",
|
|
1010
1010
|
"staff.timesheets.my.total": "Total",
|
|
@@ -1143,7 +1143,7 @@
|
|
|
1143
1143
|
"staff.timesheets.widgets.hoursByProject.loading": "Loading...",
|
|
1144
1144
|
"staff.timesheets.widgets.hoursByProject.settings.dateRange": "Date Range",
|
|
1145
1145
|
"staff.timesheets.widgets.hoursByProject.total": "Total",
|
|
1146
|
-
"staff.timesheets.widgets.timeReporting.error": "
|
|
1146
|
+
"staff.timesheets.widgets.timeReporting.error": "Timer-Status konnte nicht geladen werden",
|
|
1147
1147
|
"staff.timesheets.widgets.timeReporting.loading": "Loading...",
|
|
1148
1148
|
"staff.timesheets.widgets.timeReporting.noProjects": "No projects assigned.",
|
|
1149
1149
|
"staff.timesheets.widgets.timeReporting.notRunning": "Not running",
|
|
@@ -1153,9 +1153,9 @@
|
|
|
1153
1153
|
"staff.timesheets.widgets.timeReporting.selectProject": "Select project...",
|
|
1154
1154
|
"staff.timesheets.widgets.timeReporting.settings.description": "No additional settings. Select a project and start tracking from the widget.",
|
|
1155
1155
|
"staff.timesheets.widgets.timeReporting.start": "Start Timer",
|
|
1156
|
-
"staff.timesheets.widgets.timeReporting.startError": "
|
|
1156
|
+
"staff.timesheets.widgets.timeReporting.startError": "Timer konnte nicht gestartet werden",
|
|
1157
1157
|
"staff.timesheets.widgets.timeReporting.stop": "Stop Timer",
|
|
1158
|
-
"staff.timesheets.widgets.timeReporting.stopError": "
|
|
1158
|
+
"staff.timesheets.widgets.timeReporting.stopError": "Timer konnte nicht gestoppt werden",
|
|
1159
1159
|
"staff.timesheets.widgets.timeReporting.taskNote": "Task / Note",
|
|
1160
1160
|
"staff.timesheets.widgets.timeReporting.unknownProject": "Unknown project"
|
|
1161
1161
|
}
|
|
@@ -1002,9 +1002,9 @@
|
|
|
1002
1002
|
"staff.timesheets.my.timer.searchProject": "Search projects...",
|
|
1003
1003
|
"staff.timesheets.my.timer.selectProject": "Project",
|
|
1004
1004
|
"staff.timesheets.my.timer.start": "Start timer",
|
|
1005
|
-
"staff.timesheets.my.timer.startError": "
|
|
1005
|
+
"staff.timesheets.my.timer.startError": "No se pudo iniciar el temporizador",
|
|
1006
1006
|
"staff.timesheets.my.timer.stop": "Stop timer",
|
|
1007
|
-
"staff.timesheets.my.timer.stopError": "
|
|
1007
|
+
"staff.timesheets.my.timer.stopError": "No se pudo detener el temporizador",
|
|
1008
1008
|
"staff.timesheets.my.timerStart": "Start Timer",
|
|
1009
1009
|
"staff.timesheets.my.timerStop": "Stop Timer",
|
|
1010
1010
|
"staff.timesheets.my.total": "Total",
|
|
@@ -1143,7 +1143,7 @@
|
|
|
1143
1143
|
"staff.timesheets.widgets.hoursByProject.loading": "Loading...",
|
|
1144
1144
|
"staff.timesheets.widgets.hoursByProject.settings.dateRange": "Date Range",
|
|
1145
1145
|
"staff.timesheets.widgets.hoursByProject.total": "Total",
|
|
1146
|
-
"staff.timesheets.widgets.timeReporting.error": "
|
|
1146
|
+
"staff.timesheets.widgets.timeReporting.error": "No se pudo cargar el estado del temporizador",
|
|
1147
1147
|
"staff.timesheets.widgets.timeReporting.loading": "Loading...",
|
|
1148
1148
|
"staff.timesheets.widgets.timeReporting.noProjects": "No projects assigned.",
|
|
1149
1149
|
"staff.timesheets.widgets.timeReporting.notRunning": "Not running",
|
|
@@ -1153,9 +1153,9 @@
|
|
|
1153
1153
|
"staff.timesheets.widgets.timeReporting.selectProject": "Select project...",
|
|
1154
1154
|
"staff.timesheets.widgets.timeReporting.settings.description": "No additional settings. Select a project and start tracking from the widget.",
|
|
1155
1155
|
"staff.timesheets.widgets.timeReporting.start": "Start Timer",
|
|
1156
|
-
"staff.timesheets.widgets.timeReporting.startError": "
|
|
1156
|
+
"staff.timesheets.widgets.timeReporting.startError": "No se pudo iniciar el temporizador",
|
|
1157
1157
|
"staff.timesheets.widgets.timeReporting.stop": "Stop Timer",
|
|
1158
|
-
"staff.timesheets.widgets.timeReporting.stopError": "
|
|
1158
|
+
"staff.timesheets.widgets.timeReporting.stopError": "No se pudo detener el temporizador",
|
|
1159
1159
|
"staff.timesheets.widgets.timeReporting.taskNote": "Task / Note",
|
|
1160
1160
|
"staff.timesheets.widgets.timeReporting.unknownProject": "Unknown project"
|
|
1161
1161
|
}
|
|
@@ -1002,9 +1002,9 @@
|
|
|
1002
1002
|
"staff.timesheets.my.timer.searchProject": "Search projects...",
|
|
1003
1003
|
"staff.timesheets.my.timer.selectProject": "Project",
|
|
1004
1004
|
"staff.timesheets.my.timer.start": "Start timer",
|
|
1005
|
-
"staff.timesheets.my.timer.startError": "
|
|
1005
|
+
"staff.timesheets.my.timer.startError": "Nie udało się uruchomić licznika czasu",
|
|
1006
1006
|
"staff.timesheets.my.timer.stop": "Stop timer",
|
|
1007
|
-
"staff.timesheets.my.timer.stopError": "
|
|
1007
|
+
"staff.timesheets.my.timer.stopError": "Nie udało się zatrzymać licznika czasu",
|
|
1008
1008
|
"staff.timesheets.my.timerStart": "Start Timer",
|
|
1009
1009
|
"staff.timesheets.my.timerStop": "Stop Timer",
|
|
1010
1010
|
"staff.timesheets.my.total": "Total",
|
|
@@ -1143,7 +1143,7 @@
|
|
|
1143
1143
|
"staff.timesheets.widgets.hoursByProject.loading": "Loading...",
|
|
1144
1144
|
"staff.timesheets.widgets.hoursByProject.settings.dateRange": "Date Range",
|
|
1145
1145
|
"staff.timesheets.widgets.hoursByProject.total": "Total",
|
|
1146
|
-
"staff.timesheets.widgets.timeReporting.error": "
|
|
1146
|
+
"staff.timesheets.widgets.timeReporting.error": "Nie udało się wczytać stanu licznika czasu",
|
|
1147
1147
|
"staff.timesheets.widgets.timeReporting.loading": "Loading...",
|
|
1148
1148
|
"staff.timesheets.widgets.timeReporting.noProjects": "No projects assigned.",
|
|
1149
1149
|
"staff.timesheets.widgets.timeReporting.notRunning": "Not running",
|
|
@@ -1153,9 +1153,9 @@
|
|
|
1153
1153
|
"staff.timesheets.widgets.timeReporting.selectProject": "Select project...",
|
|
1154
1154
|
"staff.timesheets.widgets.timeReporting.settings.description": "No additional settings. Select a project and start tracking from the widget.",
|
|
1155
1155
|
"staff.timesheets.widgets.timeReporting.start": "Start Timer",
|
|
1156
|
-
"staff.timesheets.widgets.timeReporting.startError": "
|
|
1156
|
+
"staff.timesheets.widgets.timeReporting.startError": "Nie udało się uruchomić licznika czasu",
|
|
1157
1157
|
"staff.timesheets.widgets.timeReporting.stop": "Stop Timer",
|
|
1158
|
-
"staff.timesheets.widgets.timeReporting.stopError": "
|
|
1158
|
+
"staff.timesheets.widgets.timeReporting.stopError": "Nie udało się zatrzymać licznika czasu",
|
|
1159
1159
|
"staff.timesheets.widgets.timeReporting.taskNote": "Task / Note",
|
|
1160
1160
|
"staff.timesheets.widgets.timeReporting.unknownProject": "Unknown project"
|
|
1161
1161
|
}
|
|
@@ -9,6 +9,7 @@ import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
9
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
10
|
import { ProjectColorDot } from './ProjectColorDot'
|
|
11
11
|
import { startTimerEntry } from './startTimer'
|
|
12
|
+
import { resolveTimerActionError } from './timerErrors'
|
|
12
13
|
|
|
13
14
|
type ProjectOption = {
|
|
14
15
|
id: string
|
|
@@ -151,9 +152,9 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
151
152
|
setActiveEntryId(entryId)
|
|
152
153
|
setActiveProjectId(selectedProjectId)
|
|
153
154
|
startElapsedCounter(new Date().toISOString())
|
|
154
|
-
} catch {
|
|
155
|
+
} catch (err) {
|
|
155
156
|
flash(
|
|
156
|
-
t('staff.timesheets.my.timer.startError', 'Failed to start timer'),
|
|
157
|
+
resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),
|
|
157
158
|
'error',
|
|
158
159
|
)
|
|
159
160
|
} finally {
|
|
@@ -176,9 +177,9 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
176
177
|
setDescription('')
|
|
177
178
|
stopElapsedCounter()
|
|
178
179
|
onTimerStopped()
|
|
179
|
-
} catch {
|
|
180
|
+
} catch (err) {
|
|
180
181
|
flash(
|
|
181
|
-
t('staff.timesheets.my.timer.stopError', 'Failed to stop timer'),
|
|
182
|
+
resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),
|
|
182
183
|
'error',
|
|
183
184
|
)
|
|
184
185
|
} finally {
|
|
@@ -284,11 +285,10 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
284
285
|
{isRunning ? (
|
|
285
286
|
<IconButton
|
|
286
287
|
type="button"
|
|
287
|
-
variant="
|
|
288
|
+
variant="destructive"
|
|
288
289
|
size="default"
|
|
289
290
|
onClick={handleStop}
|
|
290
291
|
disabled={isStopping}
|
|
291
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
292
292
|
aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}
|
|
293
293
|
>
|
|
294
294
|
<Square className="size-4" />
|
|
@@ -296,11 +296,10 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
296
296
|
) : (
|
|
297
297
|
<IconButton
|
|
298
298
|
type="button"
|
|
299
|
-
variant="
|
|
299
|
+
variant="primary"
|
|
300
300
|
size="default"
|
|
301
301
|
onClick={handleStart}
|
|
302
302
|
disabled={isStarting || !selectedProjectId}
|
|
303
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
304
303
|
aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}
|
|
305
304
|
>
|
|
306
305
|
<Play className="size-4" />
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { normalizeCrudServerError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the message to show when a timesheet timer start/stop request
|
|
5
|
+
* fails. Prefers the localized message the server already returned (e.g. the
|
|
6
|
+
* 409 "another timer is already running" reason, surfaced by the atomic
|
|
7
|
+
* start-timer route) over a generic client-side fallback. Both the TimerBar
|
|
8
|
+
* and the dashboard time-reporting widget use this so a rejected timer action
|
|
9
|
+
* shows the specific server reason in the active locale instead of a hardcoded
|
|
10
|
+
* English string (issue #3507).
|
|
11
|
+
*
|
|
12
|
+
* The server message is only trusted when the error originated from an HTTP
|
|
13
|
+
* response (`raiseCrudError` attaches a numeric `status`); transport-level
|
|
14
|
+
* failures (no response) fall back to the localized fallback so the user never
|
|
15
|
+
* sees an untranslated network error string.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveTimerActionError(err: unknown, fallback: string): string {
|
|
18
|
+
const status = (err as { status?: unknown } | null)?.status
|
|
19
|
+
if (typeof status !== 'number') return fallback
|
|
20
|
+
const message = normalizeCrudServerError(err).message
|
|
21
|
+
const trimmed = typeof message === 'string' ? message.trim() : ''
|
|
22
|
+
return trimmed.length > 0 ? trimmed : fallback
|
|
23
|
+
}
|
|
@@ -7,6 +7,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
7
7
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
8
8
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
9
9
|
import { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'
|
|
10
|
+
import { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'
|
|
10
11
|
import { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'
|
|
11
12
|
|
|
12
13
|
type ProjectOption = { id: string; name: string; code: string | null }
|
|
@@ -153,7 +154,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
153
154
|
await loadState()
|
|
154
155
|
} catch (err) {
|
|
155
156
|
console.error('staff.timesheets.timeReporting.start', err)
|
|
156
|
-
setError(t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer'))
|
|
157
|
+
setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))
|
|
157
158
|
} finally {
|
|
158
159
|
setActionLoading(false)
|
|
159
160
|
}
|
|
@@ -167,7 +168,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
167
168
|
await loadState()
|
|
168
169
|
} catch (err) {
|
|
169
170
|
console.error('staff.timesheets.timeReporting.stop', err)
|
|
170
|
-
setError(t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer'))
|
|
171
|
+
setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))
|
|
171
172
|
} finally {
|
|
172
173
|
setActionLoading(false)
|
|
173
174
|
}
|
|
@@ -194,7 +195,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
194
195
|
if (error) {
|
|
195
196
|
return (
|
|
196
197
|
<div className="flex h-full items-center justify-center py-8">
|
|
197
|
-
<p className="text-sm text-destructive">{error}</p>
|
|
198
|
+
<p role="alert" className="text-sm text-destructive">{error}</p>
|
|
198
199
|
</div>
|
|
199
200
|
)
|
|
200
201
|
}
|
|
@@ -153,20 +153,29 @@ export default function WorkflowDefinitionsListPage() {
|
|
|
153
153
|
setDeleteTarget(null)
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const handleToggleEnabled = async (id: string, currentEnabled: boolean) => {
|
|
157
|
-
const result = await
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
const handleToggleEnabled = async (id: string, currentEnabled: boolean, updatedAt: string | null) => {
|
|
157
|
+
const result = await withScopedApiRequestHeaders(
|
|
158
|
+
buildOptimisticLockHeader(updatedAt),
|
|
159
|
+
() => apiCall(`/api/workflows/definitions/${id}`, {
|
|
160
|
+
method: 'PUT',
|
|
161
|
+
headers: { 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
enabled: !currentEnabled,
|
|
164
|
+
}),
|
|
162
165
|
}),
|
|
163
|
-
|
|
166
|
+
)
|
|
164
167
|
|
|
165
168
|
if (result.ok) {
|
|
166
169
|
flash(t('workflows.messages.updated'), 'success')
|
|
167
170
|
queryClient.invalidateQueries({ queryKey: ['workflow-definitions'] })
|
|
168
171
|
} else {
|
|
169
|
-
|
|
172
|
+
const conflictError = Object.assign(new Error(t('workflows.messages.updateFailed')), {
|
|
173
|
+
status: result.status,
|
|
174
|
+
...(result.result && typeof result.result === 'object' ? result.result : {}),
|
|
175
|
+
})
|
|
176
|
+
if (!surfaceRecordConflict(conflictError, t)) {
|
|
177
|
+
flash(t('workflows.messages.updateFailed'), 'error')
|
|
178
|
+
}
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
181
|
|
|
@@ -296,7 +305,7 @@ export default function WorkflowDefinitionsListPage() {
|
|
|
296
305
|
accessorKey: 'enabled',
|
|
297
306
|
cell: ({ row }) => (
|
|
298
307
|
<button
|
|
299
|
-
onClick={() => handleToggleEnabled(row.original.id, row.original.enabled)}
|
|
308
|
+
onClick={() => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt)}
|
|
300
309
|
className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium cursor-pointer ${
|
|
301
310
|
row.original.enabled
|
|
302
311
|
? 'bg-status-success-bg text-status-success-text hover:bg-status-success-border'
|
|
@@ -356,7 +365,7 @@ export default function WorkflowDefinitionsListPage() {
|
|
|
356
365
|
...(!isCodeOnly ? [{
|
|
357
366
|
id: row.original.enabled ? 'disable' : 'enable',
|
|
358
367
|
label: row.original.enabled ? t('common.disable') : t('common.enable'),
|
|
359
|
-
onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled),
|
|
368
|
+
onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled, row.original.updatedAt),
|
|
360
369
|
}] : []),
|
|
361
370
|
...(!isCodeOnly ? [{
|
|
362
371
|
id: 'duplicate',
|