@open-mercato/core 0.8.1-develop.7274.1.21a0083828 → 0.8.1-develop.7294.1.0ef99fc518
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/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +21 -3
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +6 -4
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js +6 -4
- package/dist/modules/customers/backend/customers/deals/map/components/DealsMapView.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js +5 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -4
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +6 -4
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +9 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +4 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/lookups.js +7 -3
- package/dist/modules/customers/components/calendar/editor/lookups.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +5 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js +4 -2
- package/dist/modules/customers/components/detail/RoleAssignmentRow.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +8 -5
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +10 -7
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +4 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js +9 -0
- package/dist/modules/customers/components/detail/schedule/fieldConfig.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/index.js +3 -1
- package/dist/modules/customers/components/detail/schedule/index.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +11 -4
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +17 -9
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +12 -7
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/assignableStaff.js +103 -22
- package/dist/modules/customers/lib/assignableStaff.js.map +2 -2
- package/dist/modules/customers/setup.js +10 -2
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/[id]/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/[id]/route.js.map +2 -2
- package/dist/modules/data_sync/api/schedules/route.js +5 -0
- package/dist/modules/data_sync/api/schedules/route.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +29 -5
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +48 -11
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/data_sync/data/validators.js +13 -2
- package/dist/modules/data_sync/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/schedule-value.js +30 -0
- package/dist/modules/data_sync/lib/schedule-value.js.map +7 -0
- package/dist/modules/eudr/data/validators.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/dashboard/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js +3 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/invite/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js +2 -1
- package/dist/modules/portal/frontend/[orgSlug]/portal/login/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/page.js.map +2 -2
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js +3 -4
- package/dist/modules/portal/frontend/[orgSlug]/portal/profile/page.js.map +2 -2
- package/dist/modules/portal/lib/navigation.js +3 -4
- package/dist/modules/portal/lib/navigation.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +79 -14
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/page.js +122 -86
- package/dist/modules/staff/backend/staff/time-tracking/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +38 -17
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js +45 -18
- package/dist/modules/staff/backend/staff/time-tracking/reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/extension-points.js +16 -0
- package/dist/modules/staff/extension-points.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.js.map +2 -2
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js +3 -4
- package/dist/modules/staff/frontend/[orgSlug]/portal/time-reports/page.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js +14 -13
- package/dist/modules/staff/lib/time-tracking-ui/ReportSheet.js.map +2 -2
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js +60 -41
- package/dist/modules/staff/lib/time-tracking-ui/TaskListView.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js +2 -2
- package/dist/modules/warranty_claims/api/ai/assess/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js +3 -4
- package/dist/modules/warranty_claims/api/ai/draft-reply/route.js.map +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js +3 -3
- package/dist/modules/warranty_claims/api/ai/suggest/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/assign/route.js +3 -3
- package/dist/modules/warranty_claims/api/assign/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js +2 -2
- package/dist/modules/warranty_claims/api/credit-memo/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js +2 -2
- package/dist/modules/warranty_claims/api/entitlement/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/events/route.js +4 -4
- package/dist/modules/warranty_claims/api/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/external/claims/route.js +3 -3
- package/dist/modules/warranty_claims/api/external/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/claims/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js +2 -2
- package/dist/modules/warranty_claims/api/portal/events/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js +2 -2
- package/dist/modules/warranty_claims/api/receiving/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js +2 -2
- package/dist/modules/warranty_claims/api/replacement-order/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js +2 -2
- package/dist/modules/warranty_claims/api/return-label/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js +2 -2
- package/dist/modules/warranty_claims/api/risk/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js +2 -2
- package/dist/modules/warranty_claims/api/sales-return/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/settings-general/route.js +4 -4
- package/dist/modules/warranty_claims/api/settings-general/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js +2 -2
- package/dist/modules/warranty_claims/api/stats/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/submit/route.js +3 -3
- package/dist/modules/warranty_claims/api/submit/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/transition/route.js +3 -3
- package/dist/modules/warranty_claims/api/transition/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js +3 -3
- package/dist/modules/warranty_claims/api/vendor-recovery/route.js.map +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js +2 -2
- package/dist/modules/warranty_claims/api/vendor-recovery-suggestions/route.js.map +2 -2
- package/dist/modules/warranty_claims/backend/page.js +4 -2
- package/dist/modules/warranty_claims/backend/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js +9 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js +4 -2
- package/dist/modules/warranty_claims/backend/warranty_claims/settings/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js +3 -4
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.js.map +2 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js +3 -2
- package/dist/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -1
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +56 -1
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/users/acl/route.ts +4 -3
- package/src/modules/auth/components/AclEditor.tsx +38 -2
- package/src/modules/auth/i18n/de.json +5 -1
- package/src/modules/auth/i18n/en.json +5 -1
- package/src/modules/auth/i18n/es.json +5 -1
- package/src/modules/auth/i18n/ko.json +5 -1
- package/src/modules/auth/i18n/pl.json +5 -1
- package/src/modules/customers/backend/customers/companies/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/map/components/DealsMapView.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/page.tsx +6 -4
- package/src/modules/customers/backend/customers/deals/pipeline/components/ChangeOwnerDialog.tsx +5 -3
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +6 -4
- package/src/modules/customers/backend/customers/people/page.tsx +6 -4
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +9 -2
- package/src/modules/customers/components/calendar/editor/hooks.ts +4 -2
- package/src/modules/customers/components/calendar/editor/lookups.ts +12 -4
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +4 -1
- package/src/modules/customers/components/detail/RoleAssignmentRow.tsx +4 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +15 -7
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +16 -7
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +4 -2
- package/src/modules/customers/components/detail/schedule/fieldConfig.ts +15 -0
- package/src/modules/customers/components/detail/schedule/index.ts +1 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +16 -9
- package/src/modules/customers/components/formConfig.tsx +35 -12
- package/src/modules/customers/data/validators.ts +14 -8
- 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/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/customers/lib/assignableStaff.ts +172 -34
- package/src/modules/customers/setup.ts +8 -0
- package/src/modules/data_sync/api/schedules/[id]/route.ts +5 -0
- package/src/modules/data_sync/api/schedules/route.ts +5 -0
- package/src/modules/data_sync/backend/data-sync/page.tsx +48 -4
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +61 -2
- package/src/modules/data_sync/data/validators.ts +26 -2
- package/src/modules/data_sync/i18n/de.json +2 -0
- package/src/modules/data_sync/i18n/en.json +2 -0
- package/src/modules/data_sync/i18n/es.json +2 -0
- package/src/modules/data_sync/i18n/ko.json +2 -0
- package/src/modules/data_sync/i18n/pl.json +2 -0
- package/src/modules/data_sync/lib/schedule-value.ts +45 -0
- package/src/modules/eudr/data/validators.ts +3 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/dashboard/page.tsx +5 -4
- package/src/modules/portal/frontend/[orgSlug]/portal/invite/page.tsx +3 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/login/page.tsx +2 -1
- package/src/modules/portal/frontend/[orgSlug]/portal/page.tsx +5 -5
- package/src/modules/portal/frontend/[orgSlug]/portal/profile/page.tsx +5 -4
- package/src/modules/portal/i18n/de.json +1 -0
- package/src/modules/portal/i18n/en.json +1 -0
- package/src/modules/portal/i18n/es.json +1 -0
- package/src/modules/portal/i18n/ko.json +1 -0
- package/src/modules/portal/i18n/pl.json +1 -0
- package/src/modules/portal/lib/navigation.ts +8 -3
- package/src/modules/query_index/lib/engine.ts +111 -16
- package/src/modules/staff/backend/staff/time-tracking/page.tsx +161 -130
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +55 -43
- package/src/modules/staff/backend/staff/time-tracking/reports/[id]/page.tsx +61 -38
- package/src/modules/staff/extension-points.ts +16 -0
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/[id]/page.tsx +3 -4
- package/src/modules/staff/frontend/[orgSlug]/portal/time-reports/page.tsx +3 -4
- package/src/modules/staff/i18n/de.json +3 -0
- package/src/modules/staff/i18n/en.json +3 -0
- package/src/modules/staff/i18n/es.json +3 -0
- package/src/modules/staff/i18n/ko.json +3 -0
- package/src/modules/staff/i18n/pl.json +3 -0
- package/src/modules/staff/lib/time-tracking-ui/ReportSheet.tsx +32 -29
- package/src/modules/staff/lib/time-tracking-ui/TaskListView.tsx +89 -79
- package/src/modules/warranty_claims/api/ai/assess/route.ts +2 -2
- package/src/modules/warranty_claims/api/ai/draft-reply/route.ts +2 -3
- package/src/modules/warranty_claims/api/ai/suggest/route.ts +3 -3
- package/src/modules/warranty_claims/api/assign/route.ts +3 -3
- package/src/modules/warranty_claims/api/credit-memo/route.ts +2 -2
- package/src/modules/warranty_claims/api/entitlement/route.ts +2 -2
- package/src/modules/warranty_claims/api/events/route.ts +4 -4
- package/src/modules/warranty_claims/api/external/claims/route.ts +3 -3
- package/src/modules/warranty_claims/api/portal/claims/[id]/submit/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/[id]/withdraw/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/claims/route.ts +2 -2
- package/src/modules/warranty_claims/api/portal/events/route.ts +2 -2
- package/src/modules/warranty_claims/api/receiving/route.ts +2 -2
- package/src/modules/warranty_claims/api/replacement-order/route.ts +2 -2
- package/src/modules/warranty_claims/api/return-label/route.ts +2 -2
- package/src/modules/warranty_claims/api/risk/route.ts +2 -2
- package/src/modules/warranty_claims/api/sales-return/route.ts +2 -2
- package/src/modules/warranty_claims/api/settings-general/route.ts +4 -4
- package/src/modules/warranty_claims/api/stats/route.ts +2 -2
- package/src/modules/warranty_claims/api/submit/route.ts +3 -3
- package/src/modules/warranty_claims/api/transition/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery/route.ts +3 -3
- package/src/modules/warranty_claims/api/vendor-recovery-suggestions/route.ts +2 -2
- package/src/modules/warranty_claims/backend/page.tsx +4 -2
- package/src/modules/warranty_claims/backend/warranty_claims/[id]/page.tsx +9 -2
- package/src/modules/warranty_claims/backend/warranty_claims/settings/page.tsx +4 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/[id]/page.tsx +5 -4
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/new/page.tsx +5 -2
- package/src/modules/warranty_claims/frontend/[orgSlug]/portal/claims/page.tsx +5 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -1
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +2 -2
- package/src/modules/workflows/i18n/de.json +1 -1
- package/src/modules/workflows/i18n/en.json +1 -1
- package/src/modules/workflows/i18n/es.json +1 -1
- package/src/modules/workflows/i18n/ko.json +1 -1
- package/src/modules/workflows/i18n/pl.json +1 -1
- package/src/modules/workflows/lib/event-trigger-service.ts +139 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
1
|
+
import { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
2
2
|
import type { FilterOption } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
3
3
|
|
|
4
4
|
export type AssignableStaffMember = {
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Real team-member id when the staff module is present; `null` when resolved
|
|
7
|
+
* via the auth-users fallback (auth user ids are not staff ids — passing this
|
|
8
|
+
* value to a staff API would produce a wrong lookup).
|
|
9
|
+
*/
|
|
10
|
+
teamMemberId: string | null
|
|
6
11
|
userId: string
|
|
7
12
|
displayName: string
|
|
8
13
|
email: string | null
|
|
@@ -16,6 +21,20 @@ type AssignableStaffResponse = {
|
|
|
16
21
|
pageSize?: number
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/** Shape returned by GET /api/auth/users — distinct from AssignableStaffResponse. */
|
|
25
|
+
type AuthUsersListResponse = {
|
|
26
|
+
items?: AuthUserListItem[]
|
|
27
|
+
total?: number
|
|
28
|
+
/** /api/auth/users does not return page/pageSize; callers fall back to passed-in values. */
|
|
29
|
+
isSuperAdmin?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type AuthUserListItem = {
|
|
33
|
+
id?: unknown
|
|
34
|
+
name?: unknown
|
|
35
|
+
email?: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
export type AssignableStaffMembersPage = {
|
|
20
39
|
items: AssignableStaffMember[]
|
|
21
40
|
/**
|
|
@@ -34,8 +53,9 @@ export type AssignableStaffMembersPage = {
|
|
|
34
53
|
// When that module is disabled, its `/api/staff/team-members/assignable` endpoint is
|
|
35
54
|
// absent and the request resolves to 404. Customers UI (deals / people / companies
|
|
36
55
|
// owner filters, role-assignment dialogs) is core and always enabled, so it must not
|
|
37
|
-
// break in that case —
|
|
38
|
-
//
|
|
56
|
+
// break in that case — fall back to tenant auth users, because `ownerUserId` is an
|
|
57
|
+
// auth user id, not a staff team-member id (issue #6183). Treat other failures as
|
|
58
|
+
// before: propagate them to the caller.
|
|
39
59
|
function isAssignableEndpointMissing(error: unknown): boolean {
|
|
40
60
|
return (
|
|
41
61
|
typeof error === 'object' &&
|
|
@@ -44,34 +64,12 @@ function isAssignableEndpointMissing(error: unknown): boolean {
|
|
|
44
64
|
)
|
|
45
65
|
}
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const page = options?.page ?? 1
|
|
52
|
-
const pageSize = options?.pageSize ?? 24
|
|
53
|
-
const params = new URLSearchParams()
|
|
54
|
-
params.set('page', String(page))
|
|
55
|
-
params.set('pageSize', String(pageSize))
|
|
56
|
-
const normalizedQuery = query.trim()
|
|
57
|
-
if (normalizedQuery.length > 0) {
|
|
58
|
-
params.set('search', normalizedQuery)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
let data: AssignableStaffResponse
|
|
62
|
-
try {
|
|
63
|
-
data = await readApiResultOrThrow<AssignableStaffResponse>(
|
|
64
|
-
`/api/staff/team-members/assignable?${params.toString()}`,
|
|
65
|
-
options?.signal ? { signal: options.signal } : undefined,
|
|
66
|
-
)
|
|
67
|
-
} catch (error) {
|
|
68
|
-
if (isAssignableEndpointMissing(error)) {
|
|
69
|
-
return { items: [], servedCount: 0, total: 0, page, pageSize }
|
|
70
|
-
}
|
|
71
|
-
throw error
|
|
72
|
-
}
|
|
67
|
+
function isAuthUsersFallbackDegradableHttpError(error: unknown): boolean {
|
|
68
|
+
const status = (error as { status?: unknown })?.status
|
|
69
|
+
return typeof status === 'number' && status >= 400 && status < 500
|
|
70
|
+
}
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
function mapStaffItems(rawItems: Array<Record<string, unknown>>): AssignableStaffMember[] {
|
|
75
73
|
const deduped = new Map<string, AssignableStaffMember>()
|
|
76
74
|
|
|
77
75
|
for (const item of rawItems) {
|
|
@@ -130,13 +128,153 @@ export async function fetchAssignableStaffMembersPage(
|
|
|
130
128
|
})
|
|
131
129
|
}
|
|
132
130
|
|
|
131
|
+
return Array.from(deduped.values())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mapAuthUsersToAssignable(rawItems: AuthUserListItem[]): AssignableStaffMember[] {
|
|
135
|
+
const deduped = new Map<string, AssignableStaffMember>()
|
|
136
|
+
|
|
137
|
+
for (const item of rawItems) {
|
|
138
|
+
if (!item || typeof item.id !== 'string' || !item.id.trim()) continue
|
|
139
|
+
const userId = item.id.trim()
|
|
140
|
+
if (deduped.has(userId)) continue
|
|
141
|
+
|
|
142
|
+
const name = typeof item.name === 'string' && item.name.trim().length > 0 ? item.name.trim() : null
|
|
143
|
+
const email = typeof item.email === 'string' && item.email.trim().length > 0 ? item.email.trim() : null
|
|
144
|
+
const displayName = name ?? email ?? userId
|
|
145
|
+
|
|
146
|
+
deduped.set(userId, {
|
|
147
|
+
// Auth user ids are not staff team-member ids. Callers must not pass this
|
|
148
|
+
// value to a staff API endpoint.
|
|
149
|
+
teamMemberId: null,
|
|
150
|
+
userId,
|
|
151
|
+
displayName,
|
|
152
|
+
email,
|
|
153
|
+
teamName: null,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return Array.from(deduped.values())
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* When the optional `staff` module is absent, resolve assignable owners from
|
|
162
|
+
* `GET /api/auth/users` so CRM apps without staff still show owner names and
|
|
163
|
+
* pickers (ownerUserId is always an auth user id).
|
|
164
|
+
*
|
|
165
|
+
* A 401/403/404 from the fallback endpoint degrades to an empty roster.
|
|
166
|
+
* A 5xx or network error propagates so the operator gets a signal.
|
|
167
|
+
* An AbortError is always rethrown so callers can distinguish cancellation.
|
|
168
|
+
*/
|
|
169
|
+
async function fetchAssignableUsersFallback(
|
|
170
|
+
query: string,
|
|
171
|
+
options: { page: number; pageSize: number; activeOrgId?: string | null; signal?: AbortSignal },
|
|
172
|
+
): Promise<AssignableStaffMembersPage> {
|
|
173
|
+
const { page, pageSize, activeOrgId, signal } = options
|
|
174
|
+
const params = new URLSearchParams()
|
|
175
|
+
params.set('page', String(page))
|
|
176
|
+
params.set('pageSize', String(pageSize))
|
|
177
|
+
// Only scope to the active organization when one is known. A null/missing
|
|
178
|
+
// orgId on the session resolves to `organizationId IS NULL` server-side,
|
|
179
|
+
// which returns only org-less users and drops all deal owners that belong
|
|
180
|
+
// to any organization.
|
|
181
|
+
if (activeOrgId) {
|
|
182
|
+
params.set('scopeToActiveOrganization', '1')
|
|
183
|
+
}
|
|
184
|
+
const normalizedQuery = query.trim()
|
|
185
|
+
if (normalizedQuery.length > 0) {
|
|
186
|
+
params.set('search', normalizedQuery)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let call: Awaited<ReturnType<typeof apiCall<AuthUsersListResponse>>> | null
|
|
190
|
+
try {
|
|
191
|
+
call = await apiCall<AuthUsersListResponse>(
|
|
192
|
+
`/api/auth/users?${params.toString()}`,
|
|
193
|
+
{ headers: { 'x-om-forbidden-redirect': '0' }, signal },
|
|
194
|
+
{ fallback: null },
|
|
195
|
+
)
|
|
196
|
+
} catch (err) {
|
|
197
|
+
// apiCall rethrows AbortError — preserve it so callers can distinguish
|
|
198
|
+
// cancellation from an empty roster.
|
|
199
|
+
if (err && typeof err === 'object' && (err as { name?: unknown }).name === 'AbortError') throw err
|
|
200
|
+
// HTTP 4xx from apiFetch (rare) degrades to an empty roster; network/5xx propagate.
|
|
201
|
+
if (isAuthUsersFallbackDegradableHttpError(err)) {
|
|
202
|
+
call = null
|
|
203
|
+
} else {
|
|
204
|
+
throw err
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!call) {
|
|
209
|
+
return { items: [], servedCount: 0, total: 0, page, pageSize }
|
|
210
|
+
}
|
|
211
|
+
// 5xx: backend is down — propagate so operators get a signal rather than silent empty pickers.
|
|
212
|
+
if (!call.ok && call.status >= 500) {
|
|
213
|
+
throw Object.assign(new Error(`[internal] Auth users lookup failed (${call.status})`), { status: call.status })
|
|
214
|
+
}
|
|
215
|
+
// 401/403/404: permission not granted or endpoint gone — degrade to empty roster.
|
|
216
|
+
if (!call.ok) {
|
|
217
|
+
return { items: [], servedCount: 0, total: 0, page, pageSize }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rawItems = Array.isArray(call.result?.items) ? (call.result.items as AuthUserListItem[]) : []
|
|
221
|
+
const items = mapAuthUsersToAssignable(rawItems)
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
items,
|
|
225
|
+
servedCount: rawItems.length,
|
|
226
|
+
total:
|
|
227
|
+
typeof call.result?.total === 'number' && Number.isFinite(call.result.total)
|
|
228
|
+
? call.result.total
|
|
229
|
+
: items.length,
|
|
230
|
+
// /api/auth/users does not return page/pageSize; fall back to the values passed in.
|
|
231
|
+
page,
|
|
232
|
+
pageSize,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function fetchAssignableStaffMembersPage(
|
|
237
|
+
query: string,
|
|
238
|
+
options?: { page?: number; pageSize?: number; activeOrgId?: string | null; signal?: AbortSignal },
|
|
239
|
+
): Promise<AssignableStaffMembersPage> {
|
|
240
|
+
const page = options?.page ?? 1
|
|
241
|
+
const pageSize = options?.pageSize ?? 24
|
|
242
|
+
const params = new URLSearchParams()
|
|
243
|
+
params.set('page', String(page))
|
|
244
|
+
params.set('pageSize', String(pageSize))
|
|
245
|
+
const normalizedQuery = query.trim()
|
|
246
|
+
if (normalizedQuery.length > 0) {
|
|
247
|
+
params.set('search', normalizedQuery)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let data: AssignableStaffResponse
|
|
251
|
+
try {
|
|
252
|
+
data = await readApiResultOrThrow<AssignableStaffResponse>(
|
|
253
|
+
`/api/staff/team-members/assignable?${params.toString()}`,
|
|
254
|
+
options?.signal ? { signal: options.signal } : undefined,
|
|
255
|
+
)
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (isAssignableEndpointMissing(error)) {
|
|
258
|
+
return fetchAssignableUsersFallback(query, {
|
|
259
|
+
page,
|
|
260
|
+
pageSize,
|
|
261
|
+
activeOrgId: options?.activeOrgId,
|
|
262
|
+
signal: options?.signal,
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
throw error
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rawItems = Array.isArray(data?.items) ? data.items : []
|
|
269
|
+
const items = mapStaffItems(rawItems)
|
|
270
|
+
|
|
133
271
|
return {
|
|
134
|
-
items
|
|
272
|
+
items,
|
|
135
273
|
servedCount: rawItems.length,
|
|
136
274
|
total:
|
|
137
275
|
typeof data?.total === 'number' && Number.isFinite(data.total)
|
|
138
276
|
? data.total
|
|
139
|
-
:
|
|
277
|
+
: items.length,
|
|
140
278
|
page:
|
|
141
279
|
typeof data?.page === 'number' && Number.isFinite(data.page)
|
|
142
280
|
? data.page
|
|
@@ -150,7 +288,7 @@ export async function fetchAssignableStaffMembersPage(
|
|
|
150
288
|
|
|
151
289
|
export async function fetchAssignableStaffMembers(
|
|
152
290
|
query: string,
|
|
153
|
-
options?: { pageSize?: number; signal?: AbortSignal },
|
|
291
|
+
options?: { pageSize?: number; activeOrgId?: string | null; signal?: AbortSignal },
|
|
154
292
|
): Promise<AssignableStaffMember[]> {
|
|
155
293
|
const result = await fetchAssignableStaffMembersPage(query, options)
|
|
156
294
|
return result.items
|
|
@@ -84,6 +84,11 @@ export const setup: ModuleSetupConfig = {
|
|
|
84
84
|
defaultRoleFeatures: {
|
|
85
85
|
admin: [
|
|
86
86
|
'customers.*',
|
|
87
|
+
// `auth.users.list` is granted alongside `customers.*` so the owner picker resolves
|
|
88
|
+
// user names in CRM-preset apps where the optional `staff` module is absent.
|
|
89
|
+
// Without it the fallback to /api/auth/users returns 403 and owners show as "Unknown".
|
|
90
|
+
// Existing tenants pick this up via `yarn mercato auth sync-role-acls`.
|
|
91
|
+
'auth.users.list',
|
|
87
92
|
],
|
|
88
93
|
employee: [
|
|
89
94
|
'customers.people.view',
|
|
@@ -103,6 +108,9 @@ export const setup: ModuleSetupConfig = {
|
|
|
103
108
|
'customers.roles.view',
|
|
104
109
|
'customers.roles.manage',
|
|
105
110
|
'customers.email.compose',
|
|
111
|
+
// Required so the staff-absent fallback to /api/auth/users succeeds for employees too.
|
|
112
|
+
// See `admin` entry above for full rationale.
|
|
113
|
+
'auth.users.list',
|
|
106
114
|
],
|
|
107
115
|
},
|
|
108
116
|
}
|
|
@@ -6,6 +6,8 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
6
6
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
7
7
|
import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
8
8
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
9
|
+
import { isInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'
|
|
10
|
+
import { buildScheduleValueErrorBody } from '../../../lib/schedule-value'
|
|
9
11
|
import { updateSyncScheduleSchema } from '../../../data/validators'
|
|
10
12
|
import type { SyncScheduleService } from '../../../lib/sync-schedule-service'
|
|
11
13
|
import { serializeSchedule } from '../serialize'
|
|
@@ -143,6 +145,9 @@ export async function PUT(req: Request, ctx: { params?: Promise<{ id?: string }>
|
|
|
143
145
|
if (isCrudHttpError(error)) {
|
|
144
146
|
return NextResponse.json(error.body, { status: error.status })
|
|
145
147
|
}
|
|
148
|
+
if (isInvalidScheduleValueError(error)) {
|
|
149
|
+
return NextResponse.json(buildScheduleValueErrorBody(error.scheduleType), { status: 422 })
|
|
150
|
+
}
|
|
146
151
|
const message = error instanceof Error ? error.message : 'Failed to update sync schedule'
|
|
147
152
|
return NextResponse.json({ error: message }, { status: 422 })
|
|
148
153
|
}
|
|
@@ -8,6 +8,8 @@ import type { SyncScheduleService } from '../../lib/sync-schedule-service'
|
|
|
8
8
|
import { serializeSchedule } from './serialize'
|
|
9
9
|
import { readOptimisticLockExpected } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
10
10
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
11
|
+
import { isInvalidScheduleValueError } from '@open-mercato/shared/lib/schedule/invalidScheduleValue'
|
|
12
|
+
import { buildScheduleValueErrorBody } from '../../lib/schedule-value'
|
|
11
13
|
import {
|
|
12
14
|
runCrudMutationGuardAfterSuccess,
|
|
13
15
|
validateCrudMutationGuard,
|
|
@@ -120,6 +122,9 @@ export async function POST(req: Request) {
|
|
|
120
122
|
if (isCrudHttpError(error)) {
|
|
121
123
|
return NextResponse.json(error.body, { status: error.status })
|
|
122
124
|
}
|
|
125
|
+
if (isInvalidScheduleValueError(error)) {
|
|
126
|
+
return NextResponse.json(buildScheduleValueErrorBody(error.scheduleType), { status: 422 })
|
|
127
|
+
}
|
|
123
128
|
const message = error instanceof Error ? error.message : 'Failed to save sync schedule'
|
|
124
129
|
return NextResponse.json({ error: message }, { status: 422 })
|
|
125
130
|
}
|
|
@@ -30,6 +30,8 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
|
|
|
30
30
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
31
31
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
32
32
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
33
|
+
import { isValidScheduleInterval } from '@open-mercato/shared/lib/schedule/interval'
|
|
34
|
+
import { hasScheduleValueFieldError } from '@open-mercato/core/modules/data_sync/lib/schedule-value'
|
|
33
35
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
34
36
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
35
37
|
import {
|
|
@@ -134,6 +136,19 @@ type SyncScheduleEditorState = {
|
|
|
134
136
|
|
|
135
137
|
const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
136
138
|
|
|
139
|
+
const SCHEDULE_VALUE_ERROR_ID = 'data-sync-dashboard-schedule-value-error'
|
|
140
|
+
|
|
141
|
+
type ScheduleValueError = 'empty' | 'format' | null
|
|
142
|
+
|
|
143
|
+
function detectScheduleValueError(
|
|
144
|
+
scheduleType: 'cron' | 'interval',
|
|
145
|
+
scheduleValue: string,
|
|
146
|
+
): ScheduleValueError {
|
|
147
|
+
if (scheduleValue.length === 0) return 'empty'
|
|
148
|
+
if (scheduleType === 'interval' && !isValidScheduleInterval(scheduleValue)) return 'format'
|
|
149
|
+
return null
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
/** Matches `runSyncSchema`'s own default, so omitting the field submits this value. */
|
|
138
153
|
const DEFAULT_BATCH_SIZE = '100'
|
|
139
154
|
|
|
@@ -190,6 +205,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
190
205
|
const [isLoadingSchedule, setIsLoadingSchedule] = React.useState(false)
|
|
191
206
|
const [isSavingSchedule, setIsSavingSchedule] = React.useState(false)
|
|
192
207
|
const [isDeletingSchedule, setIsDeletingSchedule] = React.useState(false)
|
|
208
|
+
const [scheduleValueError, setScheduleValueError] = React.useState<ScheduleValueError>(null)
|
|
193
209
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
194
210
|
const scopeVersion = useOrganizationScopeVersion()
|
|
195
211
|
const t = useT()
|
|
@@ -350,6 +366,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
350
366
|
}
|
|
351
367
|
|
|
352
368
|
const record = Array.isArray(call.result?.items) ? call.result?.items[0] : undefined
|
|
369
|
+
setScheduleValueError(null)
|
|
353
370
|
if (!record) {
|
|
354
371
|
setScheduleEditor(buildDefaultScheduleState(selectedEntityType))
|
|
355
372
|
setIsLoadingSchedule(false)
|
|
@@ -375,8 +392,17 @@ export default function SyncRunsDashboardPage() {
|
|
|
375
392
|
|
|
376
393
|
const updateScheduleEditor = React.useCallback((changes: Partial<SyncScheduleEditorState>) => {
|
|
377
394
|
setScheduleEditor((current) => ({ ...current, ...changes }))
|
|
395
|
+
if (changes.scheduleValue !== undefined || changes.scheduleType !== undefined) {
|
|
396
|
+
setScheduleValueError(null)
|
|
397
|
+
}
|
|
378
398
|
}, [])
|
|
379
399
|
|
|
400
|
+
const scheduleValueErrorMessage = scheduleValueError === 'empty'
|
|
401
|
+
? t('data_sync.dashboard.schedule.invalidValue', 'Provide a schedule value before saving.')
|
|
402
|
+
: scheduleEditor.scheduleType === 'cron'
|
|
403
|
+
? t('data_sync.dashboard.schedule.invalidCron', 'Enter a cron expression the scheduler can parse, for example `0 * * * *`.')
|
|
404
|
+
: t('data_sync.dashboard.schedule.invalidInterval', 'Enter a whole number followed by s, m, h or d (for example `15m`, `1h` or `24h`), at least one minute long.')
|
|
405
|
+
|
|
380
406
|
const handleCancel = React.useCallback(async (row: SyncRunRow) => {
|
|
381
407
|
// optimistic-lock-exempt: run lifecycle action endpoint (cancel), not a concurrent record edit
|
|
382
408
|
const call = await apiCall(`/api/data_sync/runs/${encodeURIComponent(row.id)}/cancel`, {
|
|
@@ -477,11 +503,18 @@ export default function SyncRunsDashboardPage() {
|
|
|
477
503
|
|
|
478
504
|
const handleSaveSchedule = React.useCallback(async () => {
|
|
479
505
|
if (!selectedIntegration || !selectedEntityType) return
|
|
480
|
-
|
|
481
|
-
|
|
506
|
+
const scheduleValue = scheduleEditor.scheduleValue.trim()
|
|
507
|
+
|
|
508
|
+
// The documented interval format is checked here so a value the scheduler
|
|
509
|
+
// can never run never reaches the API; cron stays server-validated and
|
|
510
|
+
// comes back through the same inline field error.
|
|
511
|
+
const valueError = detectScheduleValueError(scheduleEditor.scheduleType, scheduleValue)
|
|
512
|
+
if (valueError) {
|
|
513
|
+
setScheduleValueError(valueError)
|
|
482
514
|
return
|
|
483
515
|
}
|
|
484
516
|
|
|
517
|
+
setScheduleValueError(null)
|
|
485
518
|
setIsSavingSchedule(true)
|
|
486
519
|
try {
|
|
487
520
|
const call = await runMutation({
|
|
@@ -499,7 +532,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
499
532
|
entityType: selectedEntityType,
|
|
500
533
|
direction: selectedDirection,
|
|
501
534
|
scheduleType: scheduleEditor.scheduleType,
|
|
502
|
-
scheduleValue
|
|
535
|
+
scheduleValue,
|
|
503
536
|
timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
|
|
504
537
|
fullSync: scheduleEditor.fullSync,
|
|
505
538
|
isEnabled: scheduleEditor.isEnabled,
|
|
@@ -511,7 +544,7 @@ export default function SyncRunsDashboardPage() {
|
|
|
511
544
|
entityType: selectedEntityType,
|
|
512
545
|
direction: selectedDirection,
|
|
513
546
|
scheduleType: scheduleEditor.scheduleType,
|
|
514
|
-
scheduleValue
|
|
547
|
+
scheduleValue,
|
|
515
548
|
timezone: scheduleEditor.timezone.trim() || DEFAULT_TIMEZONE,
|
|
516
549
|
fullSync: scheduleEditor.fullSync,
|
|
517
550
|
isEnabled: scheduleEditor.isEnabled,
|
|
@@ -524,6 +557,10 @@ export default function SyncRunsDashboardPage() {
|
|
|
524
557
|
})
|
|
525
558
|
|
|
526
559
|
if (!call.ok || !call.result) {
|
|
560
|
+
if (hasScheduleValueFieldError(call.result)) {
|
|
561
|
+
setScheduleValueError('format')
|
|
562
|
+
return
|
|
563
|
+
}
|
|
527
564
|
const conflictError = Object.assign(
|
|
528
565
|
new Error((call.result as { error?: string } | null)?.error ?? t('data_sync.dashboard.schedule.error', 'Failed to save recurring schedule')),
|
|
529
566
|
{
|
|
@@ -965,7 +1002,14 @@ export default function SyncRunsDashboardPage() {
|
|
|
965
1002
|
onChange={(event) => updateScheduleEditor({ scheduleValue: event.target.value })}
|
|
966
1003
|
disabled={isLoadingSchedule || isSavingSchedule || isDeletingSchedule || !selectedIntegration || !selectedEntityType}
|
|
967
1004
|
placeholder={scheduleEditor.scheduleType === 'cron' ? '0 * * * *' : '1h'}
|
|
1005
|
+
aria-invalid={scheduleValueError ? true : undefined}
|
|
1006
|
+
aria-describedby={scheduleValueError ? SCHEDULE_VALUE_ERROR_ID : undefined}
|
|
968
1007
|
/>
|
|
1008
|
+
{scheduleValueError ? (
|
|
1009
|
+
<p id={SCHEDULE_VALUE_ERROR_ID} className="text-xs text-status-error-text">
|
|
1010
|
+
{scheduleValueErrorMessage}
|
|
1011
|
+
</p>
|
|
1012
|
+
) : null}
|
|
969
1013
|
<p className="text-xs text-muted-foreground">
|
|
970
1014
|
{scheduleEditor.scheduleType === 'cron'
|
|
971
1015
|
? t('data_sync.dashboard.schedule.cronHelp', 'Example: `0 * * * *` runs at the start of every hour.')
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
22
22
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
23
23
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
24
|
+
import { isValidScheduleInterval } from '@open-mercato/shared/lib/schedule/interval'
|
|
24
25
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
26
|
+
import { hasScheduleValueFieldError } from '../lib/schedule-value'
|
|
25
27
|
import { getSyncSummaryVariant } from '../lib/syncRunStatus'
|
|
26
28
|
import type { RunParameter } from '../lib/adapter'
|
|
27
29
|
import { getApplicableRunParameters } from '../lib/run-parameters'
|
|
@@ -122,6 +124,21 @@ function buildScheduleKey(entityType: string, direction: 'import' | 'export'): s
|
|
|
122
124
|
return `${entityType}:${direction}`
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
type ScheduleValueError = 'empty' | 'format' | undefined
|
|
128
|
+
|
|
129
|
+
function buildScheduleValueErrorId(scheduleKey: string): string {
|
|
130
|
+
return `data-sync-schedule-value-error-${scheduleKey.replace(/[^a-zA-Z0-9]+/g, '-')}`
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function detectScheduleValueError(
|
|
134
|
+
scheduleType: 'cron' | 'interval',
|
|
135
|
+
scheduleValue: string,
|
|
136
|
+
): ScheduleValueError {
|
|
137
|
+
if (scheduleValue.length === 0) return 'empty'
|
|
138
|
+
if (scheduleType === 'interval' && !isValidScheduleInterval(scheduleValue)) return 'format'
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
function buildScheduleEditors(
|
|
126
143
|
entityTypes: string[],
|
|
127
144
|
directions: Array<'import' | 'export'>,
|
|
@@ -163,6 +180,17 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
163
180
|
const [runningKey, setRunningKey] = React.useState<string | null>(null)
|
|
164
181
|
const [savingKey, setSavingKey] = React.useState<string | null>(null)
|
|
165
182
|
const [deletingKey, setDeletingKey] = React.useState<string | null>(null)
|
|
183
|
+
const [valueErrors, setValueErrors] = React.useState<Record<string, ScheduleValueError>>({})
|
|
184
|
+
|
|
185
|
+
const describeScheduleValueError = React.useCallback((
|
|
186
|
+
valueError: ScheduleValueError,
|
|
187
|
+
scheduleType: 'cron' | 'interval',
|
|
188
|
+
) => {
|
|
189
|
+
if (valueError === 'empty') return t('data_sync.dashboard.schedule.invalidValue', 'Provide a schedule value before saving.')
|
|
190
|
+
return scheduleType === 'cron'
|
|
191
|
+
? t('data_sync.dashboard.schedule.invalidCron', 'Enter a cron expression the scheduler can parse, for example `0 * * * *`.')
|
|
192
|
+
: t('data_sync.dashboard.schedule.invalidInterval', 'Enter a whole number followed by s, m, h or d (for example `15m`, `1h` or `24h`), at least one minute long.')
|
|
193
|
+
}, [t])
|
|
166
194
|
|
|
167
195
|
const load = React.useCallback(async () => {
|
|
168
196
|
setIsLoading(true)
|
|
@@ -178,6 +206,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
178
206
|
const supportedEntities = resolvedOption?.supportedEntities ?? []
|
|
179
207
|
const supportedDirections = getSupportedDirections(resolvedOption?.direction)
|
|
180
208
|
setSchedules(buildScheduleEditors(supportedEntities, supportedDirections, schedulesCall.result?.items ?? []))
|
|
209
|
+
setValueErrors({})
|
|
181
210
|
} catch (error) {
|
|
182
211
|
const message = error instanceof Error ? error.message : t('data_sync.integrationTab.loadError', 'Failed to load sync schedules.')
|
|
183
212
|
flash(message, 'error')
|
|
@@ -214,6 +243,9 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
214
243
|
...patch,
|
|
215
244
|
},
|
|
216
245
|
}))
|
|
246
|
+
if (patch.scheduleValue !== undefined || patch.scheduleType !== undefined) {
|
|
247
|
+
setValueErrors((current) => (current[key] ? { ...current, [key]: undefined } : current))
|
|
248
|
+
}
|
|
217
249
|
}, [])
|
|
218
250
|
|
|
219
251
|
const handleStartSync = React.useCallback(async (entityType: string, direction: 'import' | 'export', scheduleKey: string) => {
|
|
@@ -281,6 +313,18 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
281
313
|
|
|
282
314
|
const handleSaveSchedule = React.useCallback(async (entityType: string, direction: 'import' | 'export', scheduleKey: string) => {
|
|
283
315
|
const scheduleState = schedules[scheduleKey] ?? buildDefaultScheduleState(entityType)
|
|
316
|
+
const scheduleValue = scheduleState.scheduleValue.trim()
|
|
317
|
+
|
|
318
|
+
// The documented interval format is checked here so a value the scheduler
|
|
319
|
+
// can never run never reaches the API; cron stays server-validated and
|
|
320
|
+
// comes back through the same inline field error.
|
|
321
|
+
const valueError = detectScheduleValueError(scheduleState.scheduleType, scheduleValue)
|
|
322
|
+
if (valueError) {
|
|
323
|
+
setValueErrors((current) => ({ ...current, [scheduleKey]: valueError }))
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
setValueErrors((current) => (current[scheduleKey] ? { ...current, [scheduleKey]: undefined } : current))
|
|
284
328
|
setSavingKey(scheduleKey)
|
|
285
329
|
try {
|
|
286
330
|
// Keyed upsert (POST). When the server resolves an existing row the save
|
|
@@ -297,7 +341,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
297
341
|
entityType,
|
|
298
342
|
direction,
|
|
299
343
|
scheduleType: scheduleState.scheduleType,
|
|
300
|
-
scheduleValue
|
|
344
|
+
scheduleValue,
|
|
301
345
|
timezone: scheduleState.timezone,
|
|
302
346
|
fullSync: scheduleState.fullSync,
|
|
303
347
|
isEnabled: scheduleState.isEnabled,
|
|
@@ -309,7 +353,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
309
353
|
entityType,
|
|
310
354
|
direction,
|
|
311
355
|
scheduleType: scheduleState.scheduleType,
|
|
312
|
-
scheduleValue
|
|
356
|
+
scheduleValue,
|
|
313
357
|
timezone: scheduleState.timezone,
|
|
314
358
|
fullSync: scheduleState.fullSync,
|
|
315
359
|
isEnabled: scheduleState.isEnabled,
|
|
@@ -322,6 +366,10 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
322
366
|
})
|
|
323
367
|
|
|
324
368
|
if (!call.ok || !call.result) {
|
|
369
|
+
if (hasScheduleValueFieldError(call.result)) {
|
|
370
|
+
setValueErrors((current) => ({ ...current, [scheduleKey]: 'format' }))
|
|
371
|
+
return
|
|
372
|
+
}
|
|
325
373
|
const conflictError = Object.assign(
|
|
326
374
|
new Error((call.result as { error?: string } | null)?.error ?? 'Failed to save schedule'),
|
|
327
375
|
{
|
|
@@ -498,6 +546,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
498
546
|
const isSaving = savingKey === row.key
|
|
499
547
|
const isDeleting = deletingKey === row.key
|
|
500
548
|
const controlsDisabled = isRunning || isSaving || isDeleting
|
|
549
|
+
const valueError = valueErrors[row.key]
|
|
501
550
|
return (
|
|
502
551
|
<tr key={row.key} className="border-t align-top">
|
|
503
552
|
<td className="px-3 py-3 font-medium">{formatEntityTypeLabel(row.entityType)}</td>
|
|
@@ -525,7 +574,17 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
|
|
|
525
574
|
onChange={(event) => updateScheduleEditor(row.key, { scheduleValue: event.target.value }, row.entityType)}
|
|
526
575
|
disabled={controlsDisabled}
|
|
527
576
|
placeholder={scheduleState.scheduleType === 'cron' ? '0 * * * *' : '1h'}
|
|
577
|
+
aria-invalid={valueError ? true : undefined}
|
|
578
|
+
aria-describedby={valueError ? buildScheduleValueErrorId(row.key) : undefined}
|
|
579
|
+
aria-label={scheduleState.scheduleType === 'cron'
|
|
580
|
+
? t('data_sync.dashboard.schedule.cronValue', 'Cron expression')
|
|
581
|
+
: t('data_sync.dashboard.schedule.intervalValue', 'Interval')}
|
|
528
582
|
/>
|
|
583
|
+
{valueError ? (
|
|
584
|
+
<p id={buildScheduleValueErrorId(row.key)} className="mt-1 text-xs text-status-error-text">
|
|
585
|
+
{describeScheduleValueError(valueError, scheduleState.scheduleType)}
|
|
586
|
+
</p>
|
|
587
|
+
) : null}
|
|
529
588
|
</td>
|
|
530
589
|
<td className="px-3 py-3">
|
|
531
590
|
<Input
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { isValidScheduleInterval } from '@open-mercato/shared/lib/schedule/interval'
|
|
3
|
+
import { SCHEDULE_VALUE_FIELD, SCHEDULE_VALUE_FORMAT_MESSAGES } from '../lib/schedule-value'
|
|
2
4
|
|
|
3
5
|
export const runSyncSchema = z.object({
|
|
4
6
|
integrationId: z.string().min(1),
|
|
@@ -46,6 +48,28 @@ export const listSyncSchedulesQuerySchema = z.object({
|
|
|
46
48
|
pageSize: z.coerce.number().int().min(1).max(100).default(20),
|
|
47
49
|
})
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The documented interval format is part of the API contract, so a value the
|
|
53
|
+
* scheduler can never run (`"3600"`) is rejected here instead of surfacing an
|
|
54
|
+
* internal scheduler message from deep inside the write path. The value is
|
|
55
|
+
* checked exactly as the scheduler will receive it — surrounding whitespace is
|
|
56
|
+
* not tolerated here, because the scheduler's anchored parser would reject it
|
|
57
|
+
* later. Cron is left to that parser, which reports back through the same
|
|
58
|
+
* `scheduleValue` field error.
|
|
59
|
+
*/
|
|
60
|
+
function refineScheduleValueFormat(
|
|
61
|
+
value: { scheduleType?: 'cron' | 'interval'; scheduleValue?: string },
|
|
62
|
+
ctx: z.RefinementCtx,
|
|
63
|
+
): void {
|
|
64
|
+
if (value.scheduleType !== 'interval' || typeof value.scheduleValue !== 'string') return
|
|
65
|
+
if (isValidScheduleInterval(value.scheduleValue)) return
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: 'custom',
|
|
68
|
+
path: [SCHEDULE_VALUE_FIELD],
|
|
69
|
+
message: SCHEDULE_VALUE_FORMAT_MESSAGES.interval,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
export const createSyncScheduleSchema = z.object({
|
|
50
74
|
integrationId: z.string().min(1),
|
|
51
75
|
entityType: z.string().min(1),
|
|
@@ -55,7 +79,7 @@ export const createSyncScheduleSchema = z.object({
|
|
|
55
79
|
timezone: z.string().min(1).default('UTC'),
|
|
56
80
|
fullSync: z.boolean().default(false),
|
|
57
81
|
isEnabled: z.boolean().default(true),
|
|
58
|
-
})
|
|
82
|
+
}).superRefine(refineScheduleValueFormat)
|
|
59
83
|
|
|
60
84
|
export const updateSyncScheduleSchema = z.object({
|
|
61
85
|
integrationId: z.string().min(1).optional(),
|
|
@@ -68,7 +92,7 @@ export const updateSyncScheduleSchema = z.object({
|
|
|
68
92
|
isEnabled: z.boolean().optional(),
|
|
69
93
|
}).refine((value) => Object.keys(value).length > 0, {
|
|
70
94
|
message: 'At least one field must be updated',
|
|
71
|
-
})
|
|
95
|
+
}).superRefine(refineScheduleValueFormat)
|
|
72
96
|
|
|
73
97
|
export type CreateSyncScheduleInput = z.infer<typeof createSyncScheduleSchema>
|
|
74
98
|
export type UpdateSyncScheduleInput = z.infer<typeof updateSyncScheduleSchema>
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"data_sync.dashboard.schedule.interval": "Interval",
|
|
32
32
|
"data_sync.dashboard.schedule.intervalHelp": "Example: `1h`, `6h`, or `24h` for repeating intervals.",
|
|
33
33
|
"data_sync.dashboard.schedule.intervalValue": "Interval",
|
|
34
|
+
"data_sync.dashboard.schedule.invalidCron": "Geben Sie einen Cron-Ausdruck an, den der Scheduler verarbeiten kann, zum Beispiel `0 * * * *`.",
|
|
35
|
+
"data_sync.dashboard.schedule.invalidInterval": "Geben Sie eine ganze Zahl mit der Einheit s, m, h oder d an (zum Beispiel `15m`, `1h` oder `24h`), mindestens eine Minute lang.",
|
|
34
36
|
"data_sync.dashboard.schedule.invalidValue": "Provide a schedule value before saving.",
|
|
35
37
|
"data_sync.dashboard.schedule.lastRun": "Last scheduled run: {value}",
|
|
36
38
|
"data_sync.dashboard.schedule.neverRun": "Saved, but no scheduled execution has completed yet.",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"data_sync.dashboard.schedule.interval": "Interval",
|
|
32
32
|
"data_sync.dashboard.schedule.intervalHelp": "Example: `1h`, `6h`, or `24h` for repeating intervals.",
|
|
33
33
|
"data_sync.dashboard.schedule.intervalValue": "Interval",
|
|
34
|
+
"data_sync.dashboard.schedule.invalidCron": "Enter a cron expression the scheduler can parse, for example `0 * * * *`.",
|
|
35
|
+
"data_sync.dashboard.schedule.invalidInterval": "Enter a whole number followed by s, m, h or d (for example `15m`, `1h` or `24h`), at least one minute long.",
|
|
34
36
|
"data_sync.dashboard.schedule.invalidValue": "Provide a schedule value before saving.",
|
|
35
37
|
"data_sync.dashboard.schedule.lastRun": "Last scheduled run: {value}",
|
|
36
38
|
"data_sync.dashboard.schedule.neverRun": "Saved, but no scheduled execution has completed yet.",
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"data_sync.dashboard.schedule.interval": "Interval",
|
|
32
32
|
"data_sync.dashboard.schedule.intervalHelp": "Example: `1h`, `6h`, or `24h` for repeating intervals.",
|
|
33
33
|
"data_sync.dashboard.schedule.intervalValue": "Interval",
|
|
34
|
+
"data_sync.dashboard.schedule.invalidCron": "Introduce una expresión cron que el planificador pueda interpretar, por ejemplo `0 * * * *`.",
|
|
35
|
+
"data_sync.dashboard.schedule.invalidInterval": "Introduce un número entero seguido de s, m, h o d (por ejemplo `15m`, `1h` o `24h`), de al menos un minuto.",
|
|
34
36
|
"data_sync.dashboard.schedule.invalidValue": "Provide a schedule value before saving.",
|
|
35
37
|
"data_sync.dashboard.schedule.lastRun": "Last scheduled run: {value}",
|
|
36
38
|
"data_sync.dashboard.schedule.neverRun": "Saved, but no scheduled execution has completed yet.",
|