@open-mercato/core 0.6.2-develop.3461.1.605f31c2c9 → 0.6.2

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.
Files changed (173) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/queue-runner.js +15 -3
  3. package/dist/helpers/integration/queue-runner.js.map +2 -2
  4. package/dist/helpers/integration/queue.js +1 -1
  5. package/dist/helpers/integration/queue.js.map +2 -2
  6. package/dist/helpers/integration/salesUi.js +24 -1
  7. package/dist/helpers/integration/salesUi.js.map +2 -2
  8. package/dist/modules/auth/api/roles/acl/route.js +37 -1
  9. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  10. package/dist/modules/auth/api/roles/route.js +36 -2
  11. package/dist/modules/auth/api/roles/route.js.map +2 -2
  12. package/dist/modules/auth/api/users/acl/route.js +36 -1
  13. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  14. package/dist/modules/auth/api/users/route.js +101 -13
  15. package/dist/modules/auth/api/users/route.js.map +2 -2
  16. package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
  17. package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
  18. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  19. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  20. package/dist/modules/auth/backend/users/create/page.js +1 -1
  21. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  22. package/dist/modules/auth/backend/users/roleOptions.js +1 -1
  23. package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
  24. package/dist/modules/auth/commands/roles.js +8 -0
  25. package/dist/modules/auth/commands/roles.js.map +2 -2
  26. package/dist/modules/auth/commands/users.js +3 -0
  27. package/dist/modules/auth/commands/users.js.map +2 -2
  28. package/dist/modules/auth/components/AclEditor.js +53 -26
  29. package/dist/modules/auth/components/AclEditor.js.map +2 -2
  30. package/dist/modules/auth/frontend/login.js +35 -0
  31. package/dist/modules/auth/frontend/login.js.map +2 -2
  32. package/dist/modules/auth/lib/grantChecks.js +97 -2
  33. package/dist/modules/auth/lib/grantChecks.js.map +2 -2
  34. package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
  35. package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
  36. package/dist/modules/business_rules/lib/rule-evaluator.js +3 -3
  37. package/dist/modules/business_rules/lib/rule-evaluator.js.map +2 -2
  38. package/dist/modules/customers/ai-agents.js +10 -1
  39. package/dist/modules/customers/ai-agents.js.map +2 -2
  40. package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
  41. package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
  42. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
  43. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  44. package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
  45. package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
  46. package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
  47. package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
  48. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
  49. package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
  50. package/dist/modules/data_sync/api/options.js +2 -0
  51. package/dist/modules/data_sync/api/options.js.map +2 -2
  52. package/dist/modules/data_sync/api/run.js +77 -58
  53. package/dist/modules/data_sync/api/run.js.map +2 -2
  54. package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
  55. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  56. package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
  57. package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
  58. package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
  59. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  60. package/dist/modules/integrations/data/validators.js +9 -6
  61. package/dist/modules/integrations/data/validators.js.map +2 -2
  62. package/dist/modules/messages/api/route.js +2 -2
  63. package/dist/modules/messages/api/route.js.map +2 -2
  64. package/dist/modules/messages/commands/messages.js +42 -2
  65. package/dist/modules/messages/commands/messages.js.map +2 -2
  66. package/dist/modules/messages/components/MessagesInboxPageClient.js +70 -89
  67. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  68. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +8 -6
  69. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
  70. package/dist/modules/messages/components/inboxFilters.js +133 -0
  71. package/dist/modules/messages/components/inboxFilters.js.map +7 -0
  72. package/dist/modules/messages/components/messageListLabels.js +14 -0
  73. package/dist/modules/messages/components/messageListLabels.js.map +7 -0
  74. package/dist/modules/messages/components/useMessagesInboxBulkActions.js +47 -23
  75. package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +2 -2
  76. package/dist/modules/messages/data/validators.js +8 -6
  77. package/dist/modules/messages/data/validators.js.map +2 -2
  78. package/dist/modules/progress/api/jobs/route.js +26 -16
  79. package/dist/modules/progress/api/jobs/route.js.map +2 -2
  80. package/dist/modules/query_index/di.js +8 -6
  81. package/dist/modules/query_index/di.js.map +2 -2
  82. package/dist/modules/query_index/lib/indexer.js +12 -10
  83. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  84. package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
  85. package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
  86. package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
  87. package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
  88. package/dist/modules/sales/commands/documents.js +104 -0
  89. package/dist/modules/sales/commands/documents.js.map +2 -2
  90. package/dist/modules/sales/data/validators.js +20 -1
  91. package/dist/modules/sales/data/validators.js.map +2 -2
  92. package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
  93. package/dist/modules/sync_excel/api/import/route.js +126 -116
  94. package/dist/modules/sync_excel/api/import/route.js.map +2 -2
  95. package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
  96. package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
  97. package/dist/modules/translations/components/TranslationManager.js +9 -4
  98. package/dist/modules/translations/components/TranslationManager.js.map +2 -2
  99. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  100. package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
  101. package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
  102. package/jest.config.cjs +4 -1
  103. package/package.json +15 -16
  104. package/src/helpers/integration/queue-runner.ts +21 -2
  105. package/src/helpers/integration/queue.ts +1 -1
  106. package/src/helpers/integration/salesUi.ts +26 -1
  107. package/src/modules/auth/api/roles/acl/route.ts +39 -1
  108. package/src/modules/auth/api/roles/route.ts +38 -2
  109. package/src/modules/auth/api/users/acl/route.ts +41 -1
  110. package/src/modules/auth/api/users/route.ts +105 -12
  111. package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
  112. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  113. package/src/modules/auth/backend/users/create/page.tsx +1 -1
  114. package/src/modules/auth/backend/users/roleOptions.ts +2 -1
  115. package/src/modules/auth/commands/roles.ts +8 -0
  116. package/src/modules/auth/commands/users.ts +3 -0
  117. package/src/modules/auth/components/AclEditor.tsx +50 -24
  118. package/src/modules/auth/frontend/login.tsx +40 -0
  119. package/src/modules/auth/lib/grantChecks.ts +116 -2
  120. package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
  121. package/src/modules/business_rules/lib/rule-evaluator.ts +3 -3
  122. package/src/modules/customers/ai-agents.ts +10 -1
  123. package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
  124. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
  125. package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
  126. package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
  127. package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
  128. package/src/modules/data_sync/api/options.ts +2 -0
  129. package/src/modules/data_sync/api/run.ts +80 -61
  130. package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
  131. package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
  132. package/src/modules/data_sync/i18n/de.json +3 -0
  133. package/src/modules/data_sync/i18n/en.json +3 -0
  134. package/src/modules/data_sync/i18n/es.json +3 -0
  135. package/src/modules/data_sync/i18n/pl.json +3 -0
  136. package/src/modules/data_sync/lib/adapter.ts +9 -0
  137. package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
  138. package/src/modules/integrations/data/validators.ts +5 -4
  139. package/src/modules/messages/api/route.ts +2 -2
  140. package/src/modules/messages/commands/messages.ts +54 -2
  141. package/src/modules/messages/components/MessagesInboxPageClient.tsx +80 -92
  142. package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +9 -5
  143. package/src/modules/messages/components/inboxFilters.ts +162 -0
  144. package/src/modules/messages/components/messageListLabels.ts +29 -0
  145. package/src/modules/messages/components/useMessagesInboxBulkActions.ts +58 -26
  146. package/src/modules/messages/data/validators.ts +7 -5
  147. package/src/modules/messages/i18n/de.json +7 -4
  148. package/src/modules/messages/i18n/en.json +7 -4
  149. package/src/modules/messages/i18n/es.json +7 -4
  150. package/src/modules/messages/i18n/pl.json +7 -4
  151. package/src/modules/progress/api/jobs/route.ts +30 -20
  152. package/src/modules/query_index/di.ts +9 -7
  153. package/src/modules/query_index/lib/indexer.ts +13 -11
  154. package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
  155. package/src/modules/sales/api/order-adjustments/route.ts +2 -1
  156. package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
  157. package/src/modules/sales/commands/documents.ts +116 -0
  158. package/src/modules/sales/data/validators.ts +38 -0
  159. package/src/modules/sales/i18n/de.json +2 -0
  160. package/src/modules/sales/i18n/en.json +2 -0
  161. package/src/modules/sales/i18n/es.json +2 -0
  162. package/src/modules/sales/i18n/pl.json +2 -0
  163. package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
  164. package/src/modules/staff/i18n/de.json +1 -0
  165. package/src/modules/staff/i18n/en.json +1 -0
  166. package/src/modules/staff/i18n/es.json +1 -0
  167. package/src/modules/staff/i18n/pl.json +1 -0
  168. package/src/modules/sync_excel/api/import/route.ts +141 -131
  169. package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
  170. package/src/modules/translations/components/TranslationManager.tsx +9 -4
  171. package/src/modules/workflows/lib/activity-executor.ts +4 -3
  172. package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
  173. package/tsconfig.json +1 -0
@@ -20,76 +20,95 @@ export const openApi = {
20
20
  }
21
21
 
22
22
  export async function POST(req: Request) {
23
- const auth = await getAuthFromRequest(req)
24
- if (!auth?.tenantId || !auth.orgId) {
25
- return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
26
- }
23
+ try {
24
+ const auth = await getAuthFromRequest(req)
25
+ if (!auth?.tenantId || !auth.orgId) {
26
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
27
+ }
27
28
 
28
- const payload = await readJsonSafe(req)
29
- const parsed = runSyncSchema.safeParse(payload)
30
- if (!parsed.success) {
31
- return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
32
- }
29
+ const payload = await readJsonSafe(req)
30
+ const parsed = runSyncSchema.safeParse(payload)
31
+ if (!parsed.success) {
32
+ return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })
33
+ }
33
34
 
34
- const container = await createRequestContainer()
35
- const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
36
- const progressService = container.resolve('progressService') as ProgressService
37
- const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
35
+ const container = await createRequestContainer()
36
+ const syncRunService = container.resolve('dataSyncRunService') as SyncRunService
37
+ const progressService = container.resolve('progressService') as ProgressService
38
+ const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService
38
39
 
39
- const scope = {
40
- organizationId: auth.orgId as string,
41
- tenantId: auth.tenantId,
42
- }
40
+ const scope = {
41
+ organizationId: auth.orgId as string,
42
+ tenantId: auth.tenantId,
43
+ }
43
44
 
44
- const integration = getIntegration(parsed.data.integrationId)
45
- if (!integration?.providerKey) {
46
- return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
47
- }
45
+ const integration = getIntegration(parsed.data.integrationId)
46
+ if (!integration?.providerKey) {
47
+ return NextResponse.json({ error: 'Integration not found' }, { status: 404 })
48
+ }
48
49
 
49
- const adapter = getDataSyncAdapter(integration.providerKey)
50
- if (!adapter) {
51
- return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })
52
- }
50
+ const adapter = getDataSyncAdapter(integration.providerKey)
51
+ if (!adapter) {
52
+ return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })
53
+ }
54
+ if (adapter.runMode === 'provider') {
55
+ return NextResponse.json(
56
+ {
57
+ error: 'This integration must be started from its provider-specific import flow.',
58
+ settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,
59
+ },
60
+ { status: 422 },
61
+ )
62
+ }
53
63
 
54
- if (!adapter.supportedEntities.includes(parsed.data.entityType)) {
55
- return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
56
- }
64
+ if (!adapter.supportedEntities.includes(parsed.data.entityType)) {
65
+ return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })
66
+ }
57
67
 
58
- const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
59
- if (!integrationEnabled) {
60
- return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
61
- }
68
+ const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)
69
+ if (!integrationEnabled) {
70
+ return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })
71
+ }
62
72
 
63
- const overlap = await syncRunService.findRunningOverlap(
64
- parsed.data.integrationId,
65
- parsed.data.entityType,
66
- parsed.data.direction,
67
- scope,
68
- )
69
- if (overlap) {
70
- return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })
71
- }
73
+ const overlap = await syncRunService.findRunningOverlap(
74
+ parsed.data.integrationId,
75
+ parsed.data.entityType,
76
+ parsed.data.direction,
77
+ scope,
78
+ )
79
+ if (overlap) {
80
+ return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })
81
+ }
72
82
 
73
- const cursor = parsed.data.fullSync
74
- ? null
75
- : await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)
83
+ const cursor = parsed.data.fullSync
84
+ ? null
85
+ : await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)
76
86
 
77
- const { run, progressJob } = await startDataSyncRun({
78
- syncRunService,
79
- progressService,
80
- scope: {
81
- ...scope,
82
- userId: auth.sub,
83
- },
84
- input: {
85
- integrationId: parsed.data.integrationId,
86
- entityType: parsed.data.entityType,
87
- direction: parsed.data.direction,
88
- cursor,
89
- triggeredBy: parsed.data.triggeredBy ?? auth.sub,
90
- batchSize: parsed.data.batchSize,
91
- },
92
- })
87
+ const { run, progressJob } = await startDataSyncRun({
88
+ syncRunService,
89
+ progressService,
90
+ scope: {
91
+ ...scope,
92
+ userId: auth.sub,
93
+ },
94
+ input: {
95
+ integrationId: parsed.data.integrationId,
96
+ entityType: parsed.data.entityType,
97
+ direction: parsed.data.direction,
98
+ cursor,
99
+ triggeredBy: parsed.data.triggeredBy ?? auth.sub,
100
+ batchSize: parsed.data.batchSize,
101
+ },
102
+ })
93
103
 
94
- return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })
104
+ return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })
105
+ } catch (error) {
106
+ const message = error instanceof Error ? error.message : String(error)
107
+ const stack = error instanceof Error ? error.stack : undefined
108
+ console.error('[data_sync.run] unhandled error', { message, stack })
109
+ return NextResponse.json(
110
+ { error: 'Failed to start data sync run.', message, stack },
111
+ { status: 500 },
112
+ )
113
+ }
95
114
  }
@@ -65,6 +65,8 @@ type SyncOption = {
65
65
  description?: string | null
66
66
  providerKey?: string | null
67
67
  direction: 'import' | 'export' | 'bidirectional'
68
+ runMode?: 'generic' | 'provider'
69
+ canStartRun?: boolean
68
70
  supportedEntities: string[]
69
71
  hasCredentials: boolean
70
72
  isEnabled: boolean
@@ -602,6 +604,7 @@ export default function SyncRunsDashboardPage() {
602
604
  selectedIntegration
603
605
  && selectedEntityType
604
606
  && selectedIntegration.isEnabled
607
+ && selectedIntegration.canStartRun !== false
605
608
  && selectedIntegration.hasCredentials,
606
609
  )
607
610
  const hasSavedSchedule = Boolean(scheduleEditor.id)
@@ -975,6 +978,13 @@ export default function SyncRunsDashboardPage() {
975
978
  </AlertDescription>
976
979
  </Alert>
977
980
  ) : null}
981
+ {selectedIntegration && selectedIntegration.canStartRun === false ? (
982
+ <Alert variant="info">
983
+ <AlertDescription>
984
+ {t('data_sync.dashboard.start.providerManaged', 'This integration starts sync runs from its own setup flow. Open the integration settings page to continue.')}
985
+ </AlertDescription>
986
+ </Alert>
987
+ ) : null}
978
988
  </CardContent>
979
989
  </Card>
980
990
 
@@ -32,6 +32,8 @@ type SyncOption = {
32
32
  integrationId: string
33
33
  title: string
34
34
  direction: 'import' | 'export' | 'bidirectional'
35
+ runMode?: 'generic' | 'provider'
36
+ canStartRun?: boolean
35
37
  supportedEntities: string[]
36
38
  hasCredentials: boolean
37
39
  isEnabled: boolean
@@ -204,6 +206,10 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
204
206
  flash(t('data_sync.integrationTab.credentialsMissing', 'Configure credentials before starting a sync.'), 'error')
205
207
  return
206
208
  }
209
+ if (option?.canStartRun === false) {
210
+ flash(t('data_sync.integrationTab.providerManaged', 'Start this integration from its provider-specific setup flow.'), 'error')
211
+ return
212
+ }
207
213
 
208
214
  setRunningKey(scheduleKey)
209
215
  try {
@@ -231,7 +237,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
231
237
  } finally {
232
238
  setRunningKey(null)
233
239
  }
234
- }, [props.hasCredentials, props.integrationId, props.isEnabled, schedules, t])
240
+ }, [option?.canStartRun, props.hasCredentials, props.integrationId, props.isEnabled, schedules, t])
235
241
 
236
242
  const handleSaveSchedule = React.useCallback(async (entityType: string, direction: 'import' | 'export', scheduleKey: string) => {
237
243
  const scheduleState = schedules[scheduleKey] ?? buildDefaultScheduleState(entityType)
@@ -369,6 +375,14 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
369
375
  </Alert>
370
376
  ) : null}
371
377
 
378
+ {option.canStartRun === false ? (
379
+ <Alert variant="info">
380
+ <AlertDescription>
381
+ {t('data_sync.integrationTab.providerManagedNotice', 'This provider needs its own setup flow before a run can start. Use the provider tab on this page instead of generic schedules.')}
382
+ </AlertDescription>
383
+ </Alert>
384
+ ) : null}
385
+
372
386
  {rows.length === 0 ? (
373
387
  <Alert variant="info">
374
388
  <AlertDescription>
@@ -469,7 +483,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
469
483
  type="button"
470
484
  size="sm"
471
485
  onClick={() => void handleStartSync(row.entityType, row.direction, row.key)}
472
- disabled={controlsDisabled || !props.isEnabled || !props.hasCredentials}
486
+ disabled={controlsDisabled || !props.isEnabled || !props.hasCredentials || option.canStartRun === false}
473
487
  >
474
488
  {isRunning ? <Spinner className="mr-2 h-4 w-4" /> : <Play className="mr-2 h-4 w-4" />}
475
489
  {isRunning
@@ -481,7 +495,7 @@ export function IntegrationScheduleTab(props: IntegrationScheduleTabProps) {
481
495
  size="sm"
482
496
  variant="outline"
483
497
  onClick={() => void handleSaveSchedule(row.entityType, row.direction, row.key)}
484
- disabled={controlsDisabled}
498
+ disabled={controlsDisabled || option.canStartRun === false}
485
499
  >
486
500
  {isSaving ? <Spinner className="mr-2 h-4 w-4" /> : <Save className="mr-2 h-4 w-4" />}
487
501
  {isSaving
@@ -54,6 +54,7 @@
54
54
  "data_sync.dashboard.start.fullSync": "Run as full sync",
55
55
  "data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
56
56
  "data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
57
+ "data_sync.dashboard.start.providerManaged": "Diese Integration startet Synchronisierungsläufe über ihren eigenen Einrichtungsablauf. Öffne die Integrationseinstellungen, um fortzufahren.",
57
58
  "data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
58
59
  "data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
59
60
  "data_sync.dashboard.start.runNowTitle": "Run once now",
@@ -83,6 +84,8 @@
83
84
  "data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
84
85
  "data_sync.integrationTab.loadError": "Failed to load sync schedules.",
85
86
  "data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
87
+ "data_sync.integrationTab.providerManaged": "Starte diese Integration über ihren anbieterspezifischen Einrichtungsablauf.",
88
+ "data_sync.integrationTab.providerManagedNotice": "Dieser Anbieter benötigt einen eigenen Einrichtungsablauf, bevor ein Lauf gestartet werden kann. Verwende den Anbieter-Tab auf dieser Seite statt der generischen Zeitpläne.",
86
89
  "data_sync.integrationTab.refresh": "Refresh",
87
90
  "data_sync.integrationTab.runError": "Failed to start sync.",
88
91
  "data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
@@ -54,6 +54,7 @@
54
54
  "data_sync.dashboard.start.fullSync": "Run as full sync",
55
55
  "data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
56
56
  "data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
57
+ "data_sync.dashboard.start.providerManaged": "This integration starts sync runs from its own setup flow. Open the integration settings page to continue.",
57
58
  "data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
58
59
  "data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
59
60
  "data_sync.dashboard.start.runNowTitle": "Run once now",
@@ -83,6 +84,8 @@
83
84
  "data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
84
85
  "data_sync.integrationTab.loadError": "Failed to load sync schedules.",
85
86
  "data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
87
+ "data_sync.integrationTab.providerManaged": "Start this integration from its provider-specific setup flow.",
88
+ "data_sync.integrationTab.providerManagedNotice": "This provider needs its own setup flow before a run can start. Use the provider tab on this page instead of generic schedules.",
86
89
  "data_sync.integrationTab.refresh": "Refresh",
87
90
  "data_sync.integrationTab.runError": "Failed to start sync.",
88
91
  "data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
@@ -54,6 +54,7 @@
54
54
  "data_sync.dashboard.start.fullSync": "Run as full sync",
55
55
  "data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
56
56
  "data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
57
+ "data_sync.dashboard.start.providerManaged": "Esta integración inicia las sincronizaciones desde su propio flujo de configuración. Abre la página de ajustes de la integración para continuar.",
57
58
  "data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
58
59
  "data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
59
60
  "data_sync.dashboard.start.runNowTitle": "Run once now",
@@ -83,6 +84,8 @@
83
84
  "data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
84
85
  "data_sync.integrationTab.loadError": "Failed to load sync schedules.",
85
86
  "data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
87
+ "data_sync.integrationTab.providerManaged": "Inicia esta integración desde su flujo de configuración específico del proveedor.",
88
+ "data_sync.integrationTab.providerManagedNotice": "Este proveedor necesita su propio flujo de configuración antes de poder iniciar una ejecución. Usa la pestaña del proveedor en esta página en lugar de los calendarios genéricos.",
86
89
  "data_sync.integrationTab.refresh": "Refresh",
87
90
  "data_sync.integrationTab.runError": "Failed to start sync.",
88
91
  "data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
@@ -54,6 +54,7 @@
54
54
  "data_sync.dashboard.start.fullSync": "Run as full sync",
55
55
  "data_sync.dashboard.start.fullSyncHelp": "Ignore the saved cursor and process the entire source again for this run.",
56
56
  "data_sync.dashboard.start.invalidBatchSize": "Batch size must be between 1 and 1000.",
57
+ "data_sync.dashboard.start.providerManaged": "Ta integracja uruchamia synchronizacje z własnego przepływu konfiguracji. Otwórz ustawienia integracji, aby kontynuować.",
57
58
  "data_sync.dashboard.start.runNowDescription": "Use this for the next immediate sync. Batch size and full-sync mode apply only to this manual run.",
58
59
  "data_sync.dashboard.start.runNowFootnote": "Manual runs show progress immediately and land on the run detail page after launch.",
59
60
  "data_sync.dashboard.start.runNowTitle": "Run once now",
@@ -83,6 +84,8 @@
83
84
  "data_sync.integrationTab.integrationDisabledNotice": "The integration is disabled. You can save schedules now, but runs will stay blocked until the integration is enabled.",
84
85
  "data_sync.integrationTab.loadError": "Failed to load sync schedules.",
85
86
  "data_sync.integrationTab.notAvailable": "This integration is not registered as a data sync provider.",
87
+ "data_sync.integrationTab.providerManaged": "Uruchom tę integrację z jej przepływu konfiguracji dostawcy.",
88
+ "data_sync.integrationTab.providerManagedNotice": "Ten dostawca wymaga własnego przepływu konfiguracji, zanim będzie można uruchomić synchronizację. Użyj karty dostawcy na tej stronie zamiast ogólnych harmonogramów.",
86
89
  "data_sync.integrationTab.refresh": "Refresh",
87
90
  "data_sync.integrationTab.runError": "Failed to start sync.",
88
91
  "data_sync.integrationTab.runNowHelp": "Run now uses the full-sync checkbox from the same row and starts progress tracking immediately in Data Sync.",
@@ -93,6 +93,15 @@ export interface DataSyncAdapter {
93
93
  readonly providerKey: string
94
94
  readonly direction: 'import' | 'export' | 'bidirectional'
95
95
  readonly supportedEntities: string[]
96
+ /**
97
+ * How a run may be started.
98
+ *
99
+ * - `generic` (default): `/api/data_sync/run` has enough information to
100
+ * create and enqueue the run.
101
+ * - `provider`: the provider owns a prerequisite flow before a run can be
102
+ * enqueued, such as uploading a CSV and linking that upload to the run.
103
+ */
104
+ readonly runMode?: 'generic' | 'provider'
96
105
  readonly operationalTelemetry?: boolean
97
106
 
98
107
  streamImport?(input: StreamImportInput): AsyncIterable<ImportBatch>
@@ -442,15 +442,16 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
442
442
  )
443
443
  }, [integrationId])
444
444
 
445
- const loadDetail = React.useCallback(async () => {
445
+ const loadDetail = React.useCallback(async (options?: { showLoading?: boolean }) => {
446
+ const showLoading = options?.showLoading ?? true
446
447
  const currentIntegrationId = resolveCurrentIntegrationId()
447
448
  if (!currentIntegrationId) {
448
- setIsLoading(false)
449
- setError(t('integrations.detail.loadError', 'Failed to load integration'))
449
+ if (showLoading) setIsLoading(false)
450
+ if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
450
451
  return
451
452
  }
452
- setError(null)
453
- setIsLoading(true)
453
+ if (showLoading) setError(null)
454
+ if (showLoading) setIsLoading(true)
454
455
  try {
455
456
  const call = await apiCall<IntegrationDetail>(
456
457
  `/api/integrations/${encodeURIComponent(currentIntegrationId)}`,
@@ -458,15 +459,16 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
458
459
  { fallback: null },
459
460
  )
460
461
  if (!call.ok || !call.result) {
461
- setError(t('integrations.detail.loadError', 'Failed to load integration'))
462
- setIsLoading(false)
462
+ if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
463
+ if (showLoading) setIsLoading(false)
463
464
  return
464
465
  }
465
466
  setDetail(call.result)
466
- setIsLoading(false)
467
+ setError(null)
468
+ if (showLoading) setIsLoading(false)
467
469
  } catch {
468
- setError(t('integrations.detail.loadError', 'Failed to load integration'))
469
- setIsLoading(false)
470
+ if (showLoading) setError(t('integrations.detail.loadError', 'Failed to load integration'))
471
+ if (showLoading) setIsLoading(false)
470
472
  }
471
473
  }, [resolveCurrentIntegrationId, t])
472
474
 
@@ -522,14 +524,14 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
522
524
  spotId: detailWidgetSpotId,
523
525
  })
524
526
  const refreshDetail = React.useCallback(async () => {
525
- await loadDetail()
527
+ await loadDetail({ showLoading: false })
526
528
  await loadCredentials()
527
529
  }, [loadCredentials, loadDetail])
528
530
  const refreshLogs = React.useCallback(async () => {
529
531
  await loadLogs()
530
532
  }, [loadLogs])
531
533
  const refreshHealthSnapshot = React.useCallback(async () => {
532
- await loadDetail()
534
+ await loadDetail({ showLoading: false })
533
535
  }, [loadDetail])
534
536
  const runIdFromUrl = searchParams?.get('runId') ?? null
535
537
  const refreshRunActivity = React.useCallback(async (options?: { showLoading?: boolean }) => {
@@ -546,7 +548,7 @@ export default function IntegrationDetailPage({ params }: IntegrationDetailPageP
546
548
  { fallback: null },
547
549
  )
548
550
  await loadLogs()
549
- await loadDetail()
551
+ await loadDetail({ showLoading: false })
550
552
  if (call.ok && call.result) {
551
553
  setActiveRunDetail(call.result)
552
554
  setActiveRunRefreshedAt(new Date().toISOString())
@@ -40,14 +40,15 @@ export const listIntegrationLogsQuerySchema = z.object({
40
40
 
41
41
  export type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>
42
42
 
43
- const optionalBooleanQuery = z.union([z.boolean(), z.string(), z.null(), z.undefined()])
44
- .transform((value) => {
43
+ const optionalBooleanQuery = z.preprocess(
44
+ (value) => {
45
45
  if (value === undefined || value === '' || value === null) return undefined
46
46
  if (value === true || value === 'true' || value === '1') return true
47
47
  if (value === false || value === 'false' || value === '0') return false
48
48
  return value
49
- })
50
- .pipe(z.boolean().optional())
49
+ },
50
+ z.boolean().optional(),
51
+ )
51
52
 
52
53
  export const integrationMarketplaceHealthStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'unconfigured'])
53
54
 
@@ -168,13 +168,13 @@ export async function GET(req: Request) {
168
168
  eb.selectFrom('attachments')
169
169
  .select(sql<number>`1`.as('one'))
170
170
  .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)
171
- .whereRef('attachments.record_id', '=', 'm.id')
171
+ .where(sql<boolean>`attachments.record_id = m.id::text`)
172
172
  )
173
173
  const notExistsFn = (eb: any) => eb.not(eb.exists(
174
174
  eb.selectFrom('attachments')
175
175
  .select(sql<number>`1`.as('one'))
176
176
  .where('attachments.entity_id', '=', MESSAGE_ATTACHMENT_ENTITY_ID)
177
- .whereRef('attachments.record_id', '=', 'm.id')
177
+ .where(sql<boolean>`attachments.record_id = m.id::text`)
178
178
  ))
179
179
  q = input.hasAttachments ? q.where(existsFn) : q.where(notExistsFn)
180
180
  }
@@ -2,7 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { z } from 'zod'
3
3
  import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
4
4
  import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
- import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
6
6
  import { Message, MessageObject, MessageRecipient, type MessageActionData } from '../data/entities'
7
7
  import { emitMessagesEvent } from '../events'
8
8
  import {
@@ -368,6 +368,20 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
368
368
  if (message.senderUserId !== input.userId) throw new Error('Access denied')
369
369
  if (!message.isDraft) throw new Error('Only draft messages can be edited')
370
370
 
371
+ const isSending = input.isDraft === false
372
+ const preloadedRecipients = isSending && !input.recipients
373
+ ? await findWithDecryption(
374
+ em,
375
+ MessageRecipient,
376
+ { messageId: message.id, deletedAt: null },
377
+ undefined,
378
+ {
379
+ tenantId: input.tenantId,
380
+ organizationId: input.organizationId,
381
+ },
382
+ )
383
+ : null
384
+
371
385
  const nextMessageType = input.type ?? message.type
372
386
  if (input.objects) {
373
387
  const objectValidationError = validateMessageObjectsForType(nextMessageType, input.objects)
@@ -454,12 +468,50 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
454
468
  )
455
469
  }
456
470
 
471
+ if (isSending) {
472
+ const finalVisibility = input.visibility ?? message.visibility
473
+ const finalSubject = input.subject ?? message.subject
474
+ const finalBody = input.body ?? message.body
475
+ const finalRecipientCount = input.recipients
476
+ ? input.recipients.length
477
+ : (preloadedRecipients?.length ?? 0)
478
+
479
+ if (finalVisibility !== 'public' && finalRecipientCount === 0) {
480
+ throw new Error('at least one recipient is required')
481
+ }
482
+ if (!finalSubject?.trim()) throw new Error('subject is required')
483
+ if (!finalBody?.trim()) throw new Error('body is required')
484
+
485
+ message.isDraft = false
486
+ message.status = 'sent'
487
+ message.sentAt = new Date()
488
+ if (!message.threadId) message.threadId = message.id
489
+ }
490
+
457
491
  await em.flush()
458
492
  await emitMessageIndexUpsert(ctx.container, {
459
493
  messageId: message.id,
460
494
  tenantId: input.tenantId,
461
495
  organizationId: input.organizationId,
462
496
  })
497
+
498
+ if (isSending) {
499
+ const recipientUserIds = input.recipients
500
+ ? input.recipients.map((r) => r.userId)
501
+ : (preloadedRecipients ?? []).map((r) => r.recipientUserId)
502
+ const resolvedVisibility = input.visibility ?? message.visibility
503
+ const resolvedSendViaEmail = input.sendViaEmail ?? message.sendViaEmail
504
+ await emitMessageSentEvent(ctx.container, {
505
+ messageId: message.id,
506
+ senderUserId: input.userId,
507
+ recipientUserIds,
508
+ sendViaEmail: resolvedVisibility === 'public' ? true : resolvedSendViaEmail,
509
+ externalEmail: message.externalEmail ?? null,
510
+ tenantId: input.tenantId,
511
+ organizationId: input.organizationId,
512
+ })
513
+ }
514
+
463
515
  return { ok: true, id: message.id }
464
516
  },
465
517
  async captureAfter(rawInput, _result, ctx) {
@@ -473,7 +525,7 @@ const updateDraftCommand: CommandHandler<unknown, { ok: true; id: string }> = {
473
525
  buildLog: async ({ input, snapshots }) => {
474
526
  const parsed = updateDraftCommandSchema.parse(input)
475
527
  return {
476
- actionLabel: 'Update draft message',
528
+ actionLabel: parsed.isDraft === false ? 'Send draft message' : 'Update draft message',
477
529
  resourceKind: 'messages.message',
478
530
  resourceId: parsed.messageId,
479
531
  tenantId: parsed.tenantId,