@open-mercato/core 0.6.6-develop.6229.1.ebe6706732 → 0.6.6-develop.6245.1.2be3b151f8
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/customers/components/calendar/CalendarToolbar.js +4 -6
- package/dist/modules/customers/components/calendar/CalendarToolbar.js.map +2 -2
- package/dist/modules/customers/components/calendar/EventBlock.js +2 -6
- package/dist/modules/customers/components/calendar/EventBlock.js.map +2 -2
- package/dist/modules/customers/components/calendar/UpcomingCards.js +9 -7
- package/dist/modules/customers/components/calendar/UpcomingCards.js.map +2 -2
- package/dist/modules/customers/lib/calendar/format.js +27 -0
- package/dist/modules/customers/lib/calendar/format.js.map +7 -0
- 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 +46 -13
- 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 +43 -8
- 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/components/calendar/CalendarToolbar.tsx +4 -7
- package/src/modules/customers/components/calendar/EventBlock.tsx +2 -6
- package/src/modules/customers/components/calendar/UpcomingCards.tsx +9 -7
- 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/customers/lib/calendar/format.ts +25 -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 +57 -13
- package/src/modules/staff/lib/timesheets-ui/timerErrors.ts +23 -0
- package/src/modules/staff/widgets/dashboard/timesheets-time-reporting/widget.client.tsx +57 -9
- package/src/modules/workflows/backend/definitions/page.tsx +19 -10
|
@@ -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
|
}
|
|
@@ -7,8 +7,10 @@ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
8
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
9
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
10
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
10
11
|
import { ProjectColorDot } from './ProjectColorDot'
|
|
11
12
|
import { startTimerEntry } from './startTimer'
|
|
13
|
+
import { resolveTimerActionError } from './timerErrors'
|
|
12
14
|
|
|
13
15
|
type ProjectOption = {
|
|
14
16
|
id: string
|
|
@@ -23,6 +25,17 @@ type TimerBarProps = {
|
|
|
23
25
|
onTimerStopped: () => void
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
const TIMER_MUTATION_CONTEXT_ID = 'staff-timesheets-timer-bar'
|
|
29
|
+
|
|
30
|
+
type TimerMutationContext = {
|
|
31
|
+
formId: string
|
|
32
|
+
resourceKind: string
|
|
33
|
+
resourceId: string
|
|
34
|
+
staffMemberId: string | null
|
|
35
|
+
action: 'timer-create' | 'timer-start' | 'timer-stop'
|
|
36
|
+
retryLastMutation: () => Promise<boolean>
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
function formatElapsed(seconds: number): string {
|
|
27
40
|
const h = Math.floor(seconds / 3600)
|
|
28
41
|
const m = Math.floor((seconds % 3600) / 60)
|
|
@@ -40,6 +53,10 @@ function getToday(): string {
|
|
|
40
53
|
|
|
41
54
|
export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarProps) {
|
|
42
55
|
const t = useT()
|
|
56
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<TimerMutationContext>({
|
|
57
|
+
contextId: TIMER_MUTATION_CONTEXT_ID,
|
|
58
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
59
|
+
})
|
|
43
60
|
|
|
44
61
|
const [activeEntryId, setActiveEntryId] = useState<string | null>(null)
|
|
45
62
|
const [activeProjectId, setActiveProjectId] = useState<string | null>(null)
|
|
@@ -141,19 +158,31 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
141
158
|
setIsStarting(true)
|
|
142
159
|
try {
|
|
143
160
|
const today = getToday()
|
|
144
|
-
const
|
|
161
|
+
const startPayload = {
|
|
145
162
|
staffMemberId,
|
|
146
163
|
timeProjectId: selectedProjectId,
|
|
147
164
|
date: today,
|
|
148
165
|
notes: description || null,
|
|
166
|
+
}
|
|
167
|
+
const { id: entryId } = await runMutation({
|
|
168
|
+
operation: () => startTimerEntry(startPayload),
|
|
169
|
+
context: {
|
|
170
|
+
formId: TIMER_MUTATION_CONTEXT_ID,
|
|
171
|
+
resourceKind: 'staff.timesheets.time_entry',
|
|
172
|
+
resourceId: staffMemberId,
|
|
173
|
+
staffMemberId,
|
|
174
|
+
action: 'timer-start',
|
|
175
|
+
retryLastMutation,
|
|
176
|
+
},
|
|
177
|
+
mutationPayload: startPayload,
|
|
149
178
|
})
|
|
150
179
|
|
|
151
180
|
setActiveEntryId(entryId)
|
|
152
181
|
setActiveProjectId(selectedProjectId)
|
|
153
182
|
startElapsedCounter(new Date().toISOString())
|
|
154
|
-
} catch {
|
|
183
|
+
} catch (err) {
|
|
155
184
|
flash(
|
|
156
|
-
t('staff.timesheets.my.timer.startError', 'Failed to start timer'),
|
|
185
|
+
resolveTimerActionError(err, t('staff.timesheets.my.timer.startError', 'Failed to start timer')),
|
|
157
186
|
'error',
|
|
158
187
|
)
|
|
159
188
|
} finally {
|
|
@@ -166,19 +195,36 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
166
195
|
|
|
167
196
|
setIsStopping(true)
|
|
168
197
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
198
|
+
const stopPayload = {
|
|
199
|
+
id: activeEntryId,
|
|
200
|
+
action: 'timer-stop',
|
|
201
|
+
staffMemberId,
|
|
202
|
+
}
|
|
203
|
+
await runMutation({
|
|
204
|
+
operation: () =>
|
|
205
|
+
apiCallOrThrow(
|
|
206
|
+
`/api/staff/timesheets/time-entries/${activeEntryId}/timer-stop`,
|
|
207
|
+
{ method: 'POST' },
|
|
208
|
+
),
|
|
209
|
+
context: {
|
|
210
|
+
formId: TIMER_MUTATION_CONTEXT_ID,
|
|
211
|
+
resourceKind: 'staff.timesheets.time_entry',
|
|
212
|
+
resourceId: activeEntryId,
|
|
213
|
+
staffMemberId,
|
|
214
|
+
action: 'timer-stop',
|
|
215
|
+
retryLastMutation,
|
|
216
|
+
},
|
|
217
|
+
mutationPayload: stopPayload,
|
|
218
|
+
})
|
|
173
219
|
|
|
174
220
|
setActiveEntryId(null)
|
|
175
221
|
setActiveProjectId(null)
|
|
176
222
|
setDescription('')
|
|
177
223
|
stopElapsedCounter()
|
|
178
224
|
onTimerStopped()
|
|
179
|
-
} catch {
|
|
225
|
+
} catch (err) {
|
|
180
226
|
flash(
|
|
181
|
-
t('staff.timesheets.my.timer.stopError', 'Failed to stop timer'),
|
|
227
|
+
resolveTimerActionError(err, t('staff.timesheets.my.timer.stopError', 'Failed to stop timer')),
|
|
182
228
|
'error',
|
|
183
229
|
)
|
|
184
230
|
} finally {
|
|
@@ -284,11 +330,10 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
284
330
|
{isRunning ? (
|
|
285
331
|
<IconButton
|
|
286
332
|
type="button"
|
|
287
|
-
variant="
|
|
333
|
+
variant="destructive"
|
|
288
334
|
size="default"
|
|
289
335
|
onClick={handleStop}
|
|
290
336
|
disabled={isStopping}
|
|
291
|
-
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
292
337
|
aria-label={t('staff.timesheets.my.timer.stop', 'Stop timer')}
|
|
293
338
|
>
|
|
294
339
|
<Square className="size-4" />
|
|
@@ -296,11 +341,10 @@ export function TimerBar({ projects, staffMemberId, onTimerStopped }: TimerBarPr
|
|
|
296
341
|
) : (
|
|
297
342
|
<IconButton
|
|
298
343
|
type="button"
|
|
299
|
-
variant="
|
|
344
|
+
variant="primary"
|
|
300
345
|
size="default"
|
|
301
346
|
onClick={handleStart}
|
|
302
347
|
disabled={isStarting || !selectedProjectId}
|
|
303
|
-
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
|
304
348
|
aria-label={t('staff.timesheets.my.timer.start', 'Start timer')}
|
|
305
349
|
>
|
|
306
350
|
<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
|
+
}
|
|
@@ -6,7 +6,9 @@ import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/ap
|
|
|
6
6
|
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
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
9
10
|
import { startTimerEntry } from '../../../lib/timesheets-ui/startTimer'
|
|
11
|
+
import { resolveTimerActionError } from '../../../lib/timesheets-ui/timerErrors'
|
|
10
12
|
import { DEFAULT_SETTINGS, hydrateSettings, type TimeReportingSettings } from './config'
|
|
11
13
|
|
|
12
14
|
type ProjectOption = { id: string; name: string; code: string | null }
|
|
@@ -18,6 +20,17 @@ type TimerState = {
|
|
|
18
20
|
projectId: string | null
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
const TIMER_WIDGET_MUTATION_CONTEXT_ID = 'staff-timesheets-time-reporting-widget'
|
|
24
|
+
|
|
25
|
+
type TimerWidgetMutationContext = {
|
|
26
|
+
formId: string
|
|
27
|
+
resourceKind: string
|
|
28
|
+
resourceId: string
|
|
29
|
+
staffMemberId: string
|
|
30
|
+
action: 'timer-create' | 'timer-start' | 'timer-stop'
|
|
31
|
+
retryLastMutation: () => Promise<boolean>
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
function formatElapsed(startedAt: string): string {
|
|
22
35
|
const elapsed = Math.max(0, Date.now() - new Date(startedAt).getTime())
|
|
23
36
|
const totalSeconds = Math.floor(elapsed / 1000)
|
|
@@ -35,6 +48,10 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
35
48
|
onRefreshStateChange,
|
|
36
49
|
}) => {
|
|
37
50
|
const t = useT()
|
|
51
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<TimerWidgetMutationContext>({
|
|
52
|
+
contextId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
|
|
53
|
+
blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
54
|
+
})
|
|
38
55
|
const hydrated = React.useMemo(() => hydrateSettings(settings), [settings])
|
|
39
56
|
|
|
40
57
|
const [projects, setProjects] = React.useState<ProjectOption[]>([])
|
|
@@ -141,37 +158,68 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
141
158
|
const today = new Date().toISOString().slice(0, 10)
|
|
142
159
|
// Single atomic create+start request (issue #3311): a partial failure can
|
|
143
160
|
// no longer leave an orphaned, unstarted timer entry, and a rejected start
|
|
144
|
-
// now surfaces here instead of being silently ignored.
|
|
145
|
-
|
|
161
|
+
// now surfaces here instead of being silently ignored. The call is routed
|
|
162
|
+
// through the mutation guard (issue #3308) so global injection modules can
|
|
163
|
+
// run onBeforeSave/onAfterSave and surface conflicts consistently.
|
|
164
|
+
const startPayload = {
|
|
146
165
|
staffMemberId,
|
|
147
166
|
timeProjectId: selectedProjectId,
|
|
148
167
|
date: today,
|
|
149
168
|
notes: notes.trim() || null,
|
|
169
|
+
}
|
|
170
|
+
await runMutation({
|
|
171
|
+
operation: () => startTimerEntry(startPayload),
|
|
172
|
+
context: {
|
|
173
|
+
formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
|
|
174
|
+
resourceKind: 'staff.timesheets.time_entry',
|
|
175
|
+
resourceId: staffMemberId,
|
|
176
|
+
staffMemberId,
|
|
177
|
+
action: 'timer-start',
|
|
178
|
+
retryLastMutation,
|
|
179
|
+
},
|
|
180
|
+
mutationPayload: startPayload,
|
|
150
181
|
})
|
|
151
182
|
|
|
152
183
|
onSettingsChange({ ...hydrated, lastProjectId: selectedProjectId })
|
|
153
184
|
await loadState()
|
|
154
185
|
} catch (err) {
|
|
155
186
|
console.error('staff.timesheets.timeReporting.start', err)
|
|
156
|
-
setError(t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer'))
|
|
187
|
+
setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.startError', 'Failed to start timer')))
|
|
157
188
|
} finally {
|
|
158
189
|
setActionLoading(false)
|
|
159
190
|
}
|
|
160
|
-
}, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, t])
|
|
191
|
+
}, [selectedProjectId, staffMemberId, timer.running, notes, hydrated, onSettingsChange, loadState, runMutation, retryLastMutation, t])
|
|
161
192
|
|
|
162
193
|
const handleStop = React.useCallback(async () => {
|
|
163
|
-
if (!timer.entryId) return
|
|
194
|
+
if (!timer.entryId || !staffMemberId) return
|
|
164
195
|
setActionLoading(true)
|
|
165
196
|
try {
|
|
166
|
-
|
|
197
|
+
const stopPayload = {
|
|
198
|
+
id: timer.entryId,
|
|
199
|
+
action: 'timer-stop',
|
|
200
|
+
staffMemberId,
|
|
201
|
+
}
|
|
202
|
+
await runMutation({
|
|
203
|
+
operation: () =>
|
|
204
|
+
apiCall(`/api/staff/timesheets/time-entries/${timer.entryId}/timer-stop`, { method: 'POST' }),
|
|
205
|
+
context: {
|
|
206
|
+
formId: TIMER_WIDGET_MUTATION_CONTEXT_ID,
|
|
207
|
+
resourceKind: 'staff.timesheets.time_entry',
|
|
208
|
+
resourceId: timer.entryId,
|
|
209
|
+
staffMemberId,
|
|
210
|
+
action: 'timer-stop',
|
|
211
|
+
retryLastMutation,
|
|
212
|
+
},
|
|
213
|
+
mutationPayload: stopPayload,
|
|
214
|
+
})
|
|
167
215
|
await loadState()
|
|
168
216
|
} catch (err) {
|
|
169
217
|
console.error('staff.timesheets.timeReporting.stop', err)
|
|
170
|
-
setError(t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer'))
|
|
218
|
+
setError(resolveTimerActionError(err, t('staff.timesheets.widgets.timeReporting.stopError', 'Failed to stop timer')))
|
|
171
219
|
} finally {
|
|
172
220
|
setActionLoading(false)
|
|
173
221
|
}
|
|
174
|
-
}, [timer.entryId, loadState, t])
|
|
222
|
+
}, [timer.entryId, staffMemberId, loadState, runMutation, retryLastMutation, t])
|
|
175
223
|
|
|
176
224
|
if (mode === 'settings') {
|
|
177
225
|
return (
|
|
@@ -194,7 +242,7 @@ const TimeReportingWidget: React.FC<DashboardWidgetComponentProps<TimeReportingS
|
|
|
194
242
|
if (error) {
|
|
195
243
|
return (
|
|
196
244
|
<div className="flex h-full items-center justify-center py-8">
|
|
197
|
-
<p className="text-sm text-destructive">{error}</p>
|
|
245
|
+
<p role="alert" className="text-sm text-destructive">{error}</p>
|
|
198
246
|
</div>
|
|
199
247
|
)
|
|
200
248
|
}
|
|
@@ -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',
|