@open-mercato/core 0.6.2-develop.3467.1.2a1818709d → 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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/helpers/integration/queue-runner.js +15 -3
- package/dist/helpers/integration/queue-runner.js.map +2 -2
- package/dist/helpers/integration/queue.js +1 -1
- package/dist/helpers/integration/queue.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +24 -1
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +37 -1
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/roles/route.js +36 -2
- package/dist/modules/auth/api/roles/route.js.map +2 -2
- package/dist/modules/auth/api/users/acl/route.js +36 -1
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +101 -13
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/backend/roles/[id]/edit/page.js +32 -14
- package/dist/modules/auth/backend/roles/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/auth/backend/users/create/page.js +1 -1
- package/dist/modules/auth/backend/users/create/page.js.map +2 -2
- package/dist/modules/auth/backend/users/roleOptions.js +1 -1
- package/dist/modules/auth/backend/users/roleOptions.js.map +2 -2
- package/dist/modules/auth/commands/roles.js +8 -0
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/auth/commands/users.js +3 -0
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/auth/components/AclEditor.js +53 -26
- package/dist/modules/auth/components/AclEditor.js.map +2 -2
- package/dist/modules/auth/lib/grantChecks.js +97 -2
- package/dist/modules/auth/lib/grantChecks.js.map +2 -2
- package/dist/modules/business_rules/lib/expression-evaluator.js +9 -7
- package/dist/modules/business_rules/lib/expression-evaluator.js.map +2 -2
- package/dist/modules/customers/ai-agents.js +10 -1
- package/dist/modules/customers/ai-agents.js.map +2 -2
- package/dist/modules/customers/ai-tools/settings-pack.js +55 -1
- package/dist/modules/customers/ai-tools/settings-pack.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +13 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +4 -30
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260521175027.js +12 -0
- package/dist/modules/customers/migrations/Migration20260521175027.js.map +7 -0
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js +41 -12
- package/dist/modules/dashboards/components/WidgetVisibilityEditor.js.map +2 -2
- package/dist/modules/data_sync/api/options.js +2 -0
- package/dist/modules/data_sync/api/options.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +77 -58
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/backend/data-sync/page.js +3 -2
- package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js +8 -3
- package/dist/modules/data_sync/components/IntegrationScheduleTab.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +15 -13
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/data/validators.js +9 -6
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -2
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +42 -2
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +66 -87
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/inboxFilters.js +133 -0
- package/dist/modules/messages/components/inboxFilters.js.map +7 -0
- package/dist/modules/messages/data/validators.js +8 -6
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/progress/api/jobs/route.js +26 -16
- package/dist/modules/progress/api/jobs/route.js.map +2 -2
- package/dist/modules/query_index/di.js +8 -6
- package/dist/modules/query_index/di.js.map +2 -2
- package/dist/modules/query_index/lib/indexer.js +12 -10
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/query_index/subscribers/upsert_one.js +7 -1
- package/dist/modules/query_index/subscribers/upsert_one.js.map +2 -2
- package/dist/modules/sales/api/order-adjustments/route.js.map +2 -2
- package/dist/modules/sales/api/quote-adjustments/route.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +104 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/data/validators.js +20 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sync_excel/api/import/route.js +126 -116
- package/dist/modules/sync_excel/api/import/route.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +1 -0
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/dist/modules/translations/components/TranslationManager.js +9 -4
- package/dist/modules/translations/components/TranslationManager.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/event-trigger-service.js +23 -11
- package/dist/modules/workflows/lib/event-trigger-service.js.map +2 -2
- package/jest.config.cjs +4 -1
- package/package.json +15 -16
- package/src/helpers/integration/queue-runner.ts +21 -2
- package/src/helpers/integration/queue.ts +1 -1
- package/src/helpers/integration/salesUi.ts +26 -1
- package/src/modules/auth/api/roles/acl/route.ts +39 -1
- package/src/modules/auth/api/roles/route.ts +38 -2
- package/src/modules/auth/api/users/acl/route.ts +41 -1
- package/src/modules/auth/api/users/route.ts +105 -12
- package/src/modules/auth/backend/roles/[id]/edit/page.tsx +35 -13
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
- package/src/modules/auth/backend/users/create/page.tsx +1 -1
- package/src/modules/auth/backend/users/roleOptions.ts +2 -1
- package/src/modules/auth/commands/roles.ts +8 -0
- package/src/modules/auth/commands/users.ts +3 -0
- package/src/modules/auth/components/AclEditor.tsx +50 -24
- package/src/modules/auth/lib/grantChecks.ts +116 -2
- package/src/modules/business_rules/lib/expression-evaluator.ts +9 -13
- package/src/modules/customers/ai-agents.ts +10 -1
- package/src/modules/customers/ai-tools/settings-pack.ts +62 -1
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +18 -7
- package/src/modules/customers/lib/personCompanyLinkTable.ts +33 -41
- package/src/modules/customers/migrations/Migration20260521175027.ts +39 -0
- package/src/modules/dashboards/components/WidgetVisibilityEditor.tsx +38 -12
- package/src/modules/data_sync/api/options.ts +2 -0
- package/src/modules/data_sync/api/run.ts +80 -61
- package/src/modules/data_sync/backend/data-sync/page.tsx +10 -0
- package/src/modules/data_sync/components/IntegrationScheduleTab.tsx +17 -3
- package/src/modules/data_sync/i18n/de.json +3 -0
- package/src/modules/data_sync/i18n/en.json +3 -0
- package/src/modules/data_sync/i18n/es.json +3 -0
- package/src/modules/data_sync/i18n/pl.json +3 -0
- package/src/modules/data_sync/lib/adapter.ts +9 -0
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +15 -13
- package/src/modules/integrations/data/validators.ts +5 -4
- package/src/modules/messages/api/route.ts +2 -2
- package/src/modules/messages/commands/messages.ts +54 -2
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +75 -90
- package/src/modules/messages/components/inboxFilters.ts +162 -0
- package/src/modules/messages/data/validators.ts +7 -5
- package/src/modules/messages/i18n/de.json +5 -4
- package/src/modules/messages/i18n/en.json +5 -4
- package/src/modules/messages/i18n/es.json +5 -4
- package/src/modules/messages/i18n/pl.json +5 -4
- package/src/modules/progress/api/jobs/route.ts +30 -20
- package/src/modules/query_index/di.ts +9 -7
- package/src/modules/query_index/lib/indexer.ts +13 -11
- package/src/modules/query_index/subscribers/upsert_one.ts +9 -1
- package/src/modules/sales/api/order-adjustments/route.ts +2 -1
- package/src/modules/sales/api/quote-adjustments/route.ts +2 -1
- package/src/modules/sales/commands/documents.ts +116 -0
- package/src/modules/sales/data/validators.ts +38 -0
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/sales/lib/makeSalesLineRoute.ts +2 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/sync_excel/api/import/route.ts +141 -131
- package/src/modules/sync_excel/lib/adapters/customers.ts +1 -0
- package/src/modules/translations/components/TranslationManager.tsx +9 -4
- package/src/modules/workflows/lib/activity-executor.ts +4 -3
- package/src/modules/workflows/lib/event-trigger-service.ts +24 -11
- package/tsconfig.json +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComparisonOperator, LogicalOperator } from '../data/validators'
|
|
2
|
+
import { testLinearRegex } from '@open-mercato/shared/lib/regex/linear'
|
|
2
3
|
import { getNestedValue, resolveSpecialValue } from './value-resolver'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -319,11 +320,8 @@ function endsWith(str: any, suffix: any): boolean {
|
|
|
319
320
|
return String(str).endsWith(String(suffix))
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
/**
|
|
323
|
-
* Security limits for regex operations
|
|
324
|
-
*/
|
|
325
|
-
const REGEX_TIMEOUT_MS = 100
|
|
326
323
|
const MAX_REGEX_LENGTH = 200
|
|
324
|
+
const MAX_REGEX_INPUT_LENGTH = 10_000
|
|
327
325
|
|
|
328
326
|
/**
|
|
329
327
|
* Check if string matches regex pattern (with ReDoS protection)
|
|
@@ -345,18 +343,16 @@ function matches(str: any, pattern: any): boolean {
|
|
|
345
343
|
return false
|
|
346
344
|
}
|
|
347
345
|
|
|
348
|
-
const regex = new RegExp(patternStr)
|
|
349
346
|
const testStr = String(str)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const startTime = Date.now()
|
|
353
|
-
const result = regex.test(testStr)
|
|
354
|
-
|
|
355
|
-
if (Date.now() - startTime > REGEX_TIMEOUT_MS) {
|
|
356
|
-
throw new Error('Regex execution timeout - potential ReDoS pattern detected')
|
|
347
|
+
if (testStr.length > MAX_REGEX_INPUT_LENGTH) {
|
|
348
|
+
return false
|
|
357
349
|
}
|
|
358
350
|
|
|
359
|
-
|
|
351
|
+
const result = testLinearRegex(patternStr, testStr, {
|
|
352
|
+
maxPatternLength: MAX_REGEX_LENGTH,
|
|
353
|
+
maxInputLength: MAX_REGEX_INPUT_LENGTH,
|
|
354
|
+
})
|
|
355
|
+
return result.ok ? result.matched : false
|
|
360
356
|
} catch (error) {
|
|
361
357
|
// Log the error for debugging but don't expose to user
|
|
362
358
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -373,8 +373,12 @@ const DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [
|
|
|
373
373
|
'2. customers.update_deal_stage — propose a stage move for a stalled high-value deal.',
|
|
374
374
|
' The runtime intercepts this via the pending-action gate; do NOT claim the change',
|
|
375
375
|
' is saved until the mutation-result-card arrives.',
|
|
376
|
+
' When moving to a named pipeline stage, first call customers.list_pipeline_stages',
|
|
377
|
+
' and pass the matching UUID as toPipelineStageId. Only use toStage for top-level',
|
|
378
|
+
' status slugs such as open, won, or lost.',
|
|
376
379
|
'Secondary read tools (use when you need more detail):',
|
|
377
380
|
'- customers.list_deals, customers.get_deal, customers.list_activities',
|
|
381
|
+
'- customers.list_pipeline_stages',
|
|
378
382
|
'- search.hybrid_search, meta.describe_agent',
|
|
379
383
|
].join('\n'),
|
|
380
384
|
},
|
|
@@ -415,7 +419,10 @@ const DEAL_ANALYZER_PROMPT_SECTIONS: DealAnalyzerPromptSection[] = [
|
|
|
415
419
|
'Lead with a one-paragraph summary of the deal portfolio health, then emit one',
|
|
416
420
|
'deal card per at-risk deal (sorted by health score ascending). After the cards,',
|
|
417
421
|
'propose exactly one stage move for the highest-value stalled deal and call',
|
|
418
|
-
'customers.update_deal_stage.
|
|
422
|
+
'customers.update_deal_stage. If the recommended stage is a pipeline stage label',
|
|
423
|
+
'rather than a status slug, resolve it with customers.list_pipeline_stages first;',
|
|
424
|
+
'do not ask the operator to paste a stage id unless no matching stage exists.',
|
|
425
|
+
'Then finish with a short conclusion naming the',
|
|
419
426
|
'highest-value stalled deal, the recommended move, and the approval status.',
|
|
420
427
|
].join('\n'),
|
|
421
428
|
},
|
|
@@ -431,6 +438,7 @@ function compileDealAnalyzerPrompt(): string {
|
|
|
431
438
|
const DEAL_ANALYZER_ALLOWED_TOOLS: readonly string[] = [
|
|
432
439
|
'customers.analyze_deals',
|
|
433
440
|
'customers.update_deal_stage',
|
|
441
|
+
'customers.list_pipeline_stages',
|
|
434
442
|
'customers.list_deals',
|
|
435
443
|
'customers.get_deal',
|
|
436
444
|
'customers.list_activities',
|
|
@@ -447,6 +455,7 @@ function buildDealAnalyzerPrepareStep() {
|
|
|
447
455
|
'customers.list_deals',
|
|
448
456
|
'customers.get_deal',
|
|
449
457
|
'customers.list_activities',
|
|
458
|
+
'customers.list_pipeline_stages',
|
|
450
459
|
'search.hybrid_search',
|
|
451
460
|
'meta.describe_agent',
|
|
452
461
|
'meta.update_task_plan',
|
|
@@ -26,6 +26,10 @@ function buildScope(ctx: CustomersToolContext, tenantId: string) {
|
|
|
26
26
|
|
|
27
27
|
const getSettingsInput = z.object({}).passthrough()
|
|
28
28
|
|
|
29
|
+
const listPipelineStagesInput = z.object({
|
|
30
|
+
pipelineId: z.string().uuid().optional().describe('Optional pipeline id to restrict returned stages.'),
|
|
31
|
+
}).passthrough()
|
|
32
|
+
|
|
29
33
|
const getSettingsTool: CustomersAiToolDefinition = {
|
|
30
34
|
name: 'customers.get_settings',
|
|
31
35
|
displayName: 'Get customers module settings',
|
|
@@ -116,6 +120,63 @@ const getSettingsTool: CustomersAiToolDefinition = {
|
|
|
116
120
|
},
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
|
|
123
|
+
const listPipelineStagesTool: CustomersAiToolDefinition = {
|
|
124
|
+
name: 'customers.list_pipeline_stages',
|
|
125
|
+
displayName: 'List customer pipeline stages',
|
|
126
|
+
description:
|
|
127
|
+
'Return customer deal pipelines and their pipeline stages, including stage ids. Use this before moving a deal to a named pipeline stage.',
|
|
128
|
+
inputSchema: listPipelineStagesInput,
|
|
129
|
+
requiredFeatures: ['customers.pipelines.view'],
|
|
130
|
+
tags: ['read', 'customers', 'pipelines'],
|
|
131
|
+
isMutation: false,
|
|
132
|
+
handler: async (rawInput, ctx) => {
|
|
133
|
+
const { tenantId } = assertTenantScope(ctx)
|
|
134
|
+
const input = listPipelineStagesInput.parse(rawInput)
|
|
135
|
+
const em = resolveEm(ctx)
|
|
136
|
+
const where: Record<string, unknown> = { tenantId }
|
|
137
|
+
if (ctx.organizationId) where.organizationId = ctx.organizationId
|
|
138
|
+
const stageWhere: Record<string, unknown> = { ...where }
|
|
139
|
+
if (input.pipelineId) stageWhere.pipelineId = input.pipelineId
|
|
140
|
+
const [pipelines, stages] = await Promise.all([
|
|
141
|
+
findWithDecryption<CustomerPipeline>(
|
|
142
|
+
em,
|
|
143
|
+
CustomerPipeline,
|
|
144
|
+
where as any,
|
|
145
|
+
{ orderBy: { createdAt: 'asc' } as any } as any,
|
|
146
|
+
buildScope(ctx, tenantId),
|
|
147
|
+
),
|
|
148
|
+
findWithDecryption<CustomerPipelineStage>(
|
|
149
|
+
em,
|
|
150
|
+
CustomerPipelineStage,
|
|
151
|
+
stageWhere as any,
|
|
152
|
+
{ orderBy: { pipelineId: 'asc', order: 'asc' } as any } as any,
|
|
153
|
+
buildScope(ctx, tenantId),
|
|
154
|
+
),
|
|
155
|
+
])
|
|
156
|
+
return {
|
|
157
|
+
pipelines: pipelines
|
|
158
|
+
.filter((row) => row.tenantId === tenantId)
|
|
159
|
+
.map((row) => ({
|
|
160
|
+
id: row.id,
|
|
161
|
+
name: row.name,
|
|
162
|
+
isDefault: !!row.isDefault,
|
|
163
|
+
organizationId: row.organizationId ?? null,
|
|
164
|
+
tenantId: row.tenantId ?? null,
|
|
165
|
+
})),
|
|
166
|
+
pipelineStages: stages
|
|
167
|
+
.filter((row) => row.tenantId === tenantId)
|
|
168
|
+
.map((row) => ({
|
|
169
|
+
id: row.id,
|
|
170
|
+
pipelineId: row.pipelineId,
|
|
171
|
+
label: row.label,
|
|
172
|
+
order: row.order,
|
|
173
|
+
organizationId: row.organizationId ?? null,
|
|
174
|
+
tenantId: row.tenantId ?? null,
|
|
175
|
+
})),
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const settingsAiTools: CustomersAiToolDefinition[] = [getSettingsTool, listPipelineStagesTool]
|
|
120
181
|
|
|
121
182
|
export default settingsAiTools
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { format } from 'date-fns'
|
|
3
2
|
import type { ActivityType } from './fieldConfig'
|
|
4
3
|
|
|
5
4
|
export type RsvpStatus = 'pending' | 'accepted' | 'declined' | 'tentative'
|
|
@@ -63,6 +62,18 @@ const DEFAULT_REMINDER_MINUTES: Record<ActivityType, number> = {
|
|
|
63
62
|
email: 15,
|
|
64
63
|
}
|
|
65
64
|
|
|
65
|
+
function padDatePart(value: number): string {
|
|
66
|
+
return String(value).padStart(2, '0')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatLocalDateInput(date: Date): string {
|
|
70
|
+
return `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatLocalTimeInput(date: Date): string {
|
|
74
|
+
return `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
|
75
|
+
}
|
|
76
|
+
|
|
66
77
|
interface UseScheduleFormStateParams {
|
|
67
78
|
open: boolean
|
|
68
79
|
editData: ScheduleActivityEditData | null | undefined
|
|
@@ -71,7 +82,7 @@ interface UseScheduleFormStateParams {
|
|
|
71
82
|
export function useScheduleFormState({ open, editData }: UseScheduleFormStateParams) {
|
|
72
83
|
const [activityType, setActivityType] = React.useState<ActivityType>('meeting')
|
|
73
84
|
const [title, setTitle] = React.useState('')
|
|
74
|
-
const [date, setDate] = React.useState(() =>
|
|
85
|
+
const [date, setDate] = React.useState(() => formatLocalDateInput(new Date()))
|
|
75
86
|
const [startTime, setStartTime] = React.useState('10:00')
|
|
76
87
|
const [duration, setDuration] = React.useState(30)
|
|
77
88
|
const [allDay, setAllDay] = React.useState(false)
|
|
@@ -101,15 +112,15 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
101
112
|
// For historical activities the canonical timestamp is `occurredAt`; for
|
|
102
113
|
// planned/future ones it's `scheduledAt`. Without this fallback editing a
|
|
103
114
|
// past activity prefilled to "today" instead of its actual moment (#1807).
|
|
104
|
-
//
|
|
105
|
-
//
|
|
115
|
+
// Keep seed values in the user's local timezone, matching the cluster-E
|
|
116
|
+
// local-day convention.
|
|
106
117
|
const sourceTimestamp = editData.occurredAt ?? editData.scheduledAt ?? null
|
|
107
118
|
const seedDate = sourceTimestamp ? new Date(sourceTimestamp) : new Date()
|
|
108
119
|
const seedDateValid = !Number.isNaN(seedDate.getTime())
|
|
109
120
|
const fallbackNow = new Date()
|
|
110
121
|
const dateForForm = seedDateValid ? seedDate : fallbackNow
|
|
111
|
-
setDate(
|
|
112
|
-
setStartTime(
|
|
122
|
+
setDate(formatLocalDateInput(dateForForm))
|
|
123
|
+
setStartTime(formatLocalTimeInput(dateForForm))
|
|
113
124
|
setDuration(editData.durationMinutes ?? 30)
|
|
114
125
|
setAllDay(editData.allDay ?? false)
|
|
115
126
|
setDescription(editData.body ?? '')
|
|
@@ -170,7 +181,7 @@ export function useScheduleFormState({ open, editData }: UseScheduleFormStatePar
|
|
|
170
181
|
// Create mode: reset all fields
|
|
171
182
|
setActivityType('meeting')
|
|
172
183
|
setTitle('')
|
|
173
|
-
setDate(
|
|
184
|
+
setDate(formatLocalDateInput(new Date()))
|
|
174
185
|
setStartTime('10:00')
|
|
175
186
|
setDuration(30)
|
|
176
187
|
setAllDay(false)
|
|
@@ -1,54 +1,46 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
-
import { sql } from '@mikro-orm/postgresql'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// The previous implementation probed `information_schema.columns` on every
|
|
4
|
+
// first call via `em.getKysely()` to decide whether to add a `deletedAt: null`
|
|
5
|
+
// filter. When that probe ran inside an already-active MikroORM transaction it
|
|
6
|
+
// shared the transaction's connection — and any failure (including transient
|
|
7
|
+
// connection-pool state, an aborted TX from a sibling query, or pg returning
|
|
8
|
+
// 25P02) propagated back into the host transaction, poisoning every following
|
|
9
|
+
// query with `current transaction is aborted, commands ignored until end of
|
|
10
|
+
// transaction block`. That is what produced the standalone-CI failure burst
|
|
11
|
+
// (loadPersonCompanyLinks → 25P02 → customers.people.create → 500) and the
|
|
12
|
+
// sync_excel worker `failedCount: 1` regression.
|
|
13
|
+
//
|
|
14
|
+
// The `deleted_at` column on `customer_person_company_links` has been part of
|
|
15
|
+
// the committed module snapshot since `Migration20260415095203` (April 2026)
|
|
16
|
+
// and is referenced by `Migration20260417140000` (partial unique index uses
|
|
17
|
+
// `where deleted_at is null`). Any environment running these migrations
|
|
18
|
+
// already has the column. So the probe is no longer load-bearing — it only
|
|
19
|
+
// adds risk. We now unconditionally include `deletedAt: null` in the filter.
|
|
20
|
+
// If a downstream DB is genuinely un-migrated, the underlying `em.find` will
|
|
21
|
+
// surface the authentic `column "deleted_at" of relation
|
|
22
|
+
// "customer_person_company_links" does not exist` (pg 42703) pointing the
|
|
23
|
+
// operator at the real fix (`yarn db:migrate`).
|
|
24
|
+
//
|
|
25
|
+
// `customerPersonCompanyLinksSupportDeletedAt` and
|
|
26
|
+
// `warnMissingCustomerPersonCompanyLinksDeletedAt` are kept as exported
|
|
27
|
+
// no-op-equivalents so external callers (and the published @open-mercato/core
|
|
28
|
+
// API surface) don't break.
|
|
6
29
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export async function customerPersonCompanyLinksSupportDeletedAt(em: EntityManager): Promise<boolean> {
|
|
11
|
-
if (typeof (em as { getKysely?: unknown }).getKysely !== 'function') {
|
|
12
|
-
return true
|
|
13
|
-
}
|
|
14
|
-
if (!supportsDeletedAtColumnPromise) {
|
|
15
|
-
const db = em.getKysely<any>() as any
|
|
16
|
-
const probe: Promise<boolean> = db
|
|
17
|
-
.selectFrom('information_schema.columns')
|
|
18
|
-
.select(['column_name'])
|
|
19
|
-
.where(sql<boolean>`table_schema = current_schema()`)
|
|
20
|
-
.where('table_name', '=', PERSON_COMPANY_LINKS_TABLE)
|
|
21
|
-
.where('column_name', '=', PERSON_COMPANY_LINKS_DELETED_AT_COLUMN)
|
|
22
|
-
.executeTakeFirst()
|
|
23
|
-
.then((row: unknown) => !!row)
|
|
24
|
-
.catch(() => false)
|
|
25
|
-
supportsDeletedAtColumnPromise = probe
|
|
26
|
-
return probe
|
|
27
|
-
}
|
|
28
|
-
return supportsDeletedAtColumnPromise
|
|
30
|
+
export async function customerPersonCompanyLinksSupportDeletedAt(_em: EntityManager): Promise<boolean> {
|
|
31
|
+
return true
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
export function warnMissingCustomerPersonCompanyLinksDeletedAt(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
warnedAboutMissingDeletedAtColumn = true
|
|
36
|
-
console.warn(
|
|
37
|
-
`[${source}] missing ${PERSON_COMPANY_LINKS_TABLE}.${PERSON_COMPANY_LINKS_DELETED_AT_COLUMN}; ` +
|
|
38
|
-
'continuing without link soft-delete filtering. Run yarn db:migrate.',
|
|
39
|
-
)
|
|
34
|
+
export function warnMissingCustomerPersonCompanyLinksDeletedAt(_source: string): void {
|
|
35
|
+
// No-op: column is guaranteed present by Migration20260415095203 and the
|
|
36
|
+
// module snapshot. Kept for backward-compatible exports.
|
|
40
37
|
}
|
|
41
38
|
|
|
42
39
|
export async function withActiveCustomerPersonCompanyLinkFilter<T extends Record<string, unknown>>(
|
|
43
|
-
|
|
40
|
+
_em: EntityManager,
|
|
44
41
|
where: T,
|
|
45
|
-
|
|
42
|
+
_source: string,
|
|
46
43
|
): Promise<T & { deletedAt?: null }> {
|
|
47
|
-
const supportsDeletedAt = await customerPersonCompanyLinksSupportDeletedAt(em)
|
|
48
|
-
if (!supportsDeletedAt) {
|
|
49
|
-
warnMissingCustomerPersonCompanyLinksDeletedAt(source)
|
|
50
|
-
return { ...where }
|
|
51
|
-
}
|
|
52
44
|
return { ...where, deletedAt: null }
|
|
53
45
|
}
|
|
54
46
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Idempotent backstop for `customer_person_company_links.deleted_at`.
|
|
5
|
+
*
|
|
6
|
+
* The column was originally added by `Migration20260415095203` and is
|
|
7
|
+
* referenced by `Migration20260417140000`'s partial unique index
|
|
8
|
+
* (`where "deleted_at" is null`). It is part of the committed
|
|
9
|
+
* `migrations/.snapshot-open-mercato.json`.
|
|
10
|
+
*
|
|
11
|
+
* However, the runtime defensive probe that lived in
|
|
12
|
+
* `packages/core/src/modules/customers/lib/personCompanyLinkTable.ts`
|
|
13
|
+
* historically existed because an aborted-transaction state on a
|
|
14
|
+
* shared connection could (very rarely) make `loadPersonCompanyLinks`
|
|
15
|
+
* fail in a way that *looked* like the column was missing — and the
|
|
16
|
+
* cached fallback then silently dropped the `deletedAt: null` filter.
|
|
17
|
+
* That probe is removed in the same change as this migration (no
|
|
18
|
+
* Kysely query is ever issued from inside an active MikroORM TX);
|
|
19
|
+
* this migration is the matching belt-and-suspenders guarantee that
|
|
20
|
+
* the column is physically present so the unconditional `deletedAt:
|
|
21
|
+
* null` filter cannot 42703.
|
|
22
|
+
*
|
|
23
|
+
* Postgres-native `add column if not exists` is used so this re-applies
|
|
24
|
+
* cleanly on already-migrated databases without raising.
|
|
25
|
+
*/
|
|
26
|
+
export class Migration20260521175027 extends Migration {
|
|
27
|
+
|
|
28
|
+
override async up(): Promise<void> {
|
|
29
|
+
this.addSql(`alter table "customer_person_company_links" add column if not exists "deleted_at" timestamptz null;`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override async down(): Promise<void> {
|
|
33
|
+
// No-op on the way down: dropping the column would break
|
|
34
|
+
// `Migration20260417140000`'s partial unique index, which is the
|
|
35
|
+
// authoritative owner of the column going forward. The original
|
|
36
|
+
// `Migration20260415095203` down still drops it for full rollback.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
}
|
|
@@ -32,6 +32,7 @@ type UserResponse = {
|
|
|
32
32
|
type BaseProps = {
|
|
33
33
|
tenantId?: string | null
|
|
34
34
|
organizationId?: string | null
|
|
35
|
+
preserveOnTenantChange?: boolean
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
type RoleProps = BaseProps & {
|
|
@@ -92,7 +93,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
92
93
|
(widget: WidgetCatalogItem) => resolveWidgetText(t, widget.id, 'description', widget.description || ''),
|
|
93
94
|
[t],
|
|
94
95
|
)
|
|
95
|
-
const { kind, targetId, tenantId, organizationId } = props
|
|
96
|
+
const { kind, targetId, tenantId, organizationId, preserveOnTenantChange = false } = props
|
|
96
97
|
const [catalog, setCatalog] = React.useState<WidgetCatalogItem[]>([])
|
|
97
98
|
const [loading, setLoading] = React.useState(true)
|
|
98
99
|
const [saving, setSaving] = React.useState(false)
|
|
@@ -136,10 +137,16 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
136
137
|
setCatalog(mapped)
|
|
137
138
|
}, [])
|
|
138
139
|
|
|
139
|
-
const
|
|
140
|
+
const tenantIdRef = React.useRef(tenantId)
|
|
141
|
+
React.useEffect(() => { tenantIdRef.current = tenantId }, [tenantId])
|
|
142
|
+
const organizationIdRef = React.useRef(organizationId)
|
|
143
|
+
React.useEffect(() => { organizationIdRef.current = organizationId }, [organizationId])
|
|
144
|
+
const hasMountedRef = React.useRef(false)
|
|
145
|
+
|
|
146
|
+
const loadRoleData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
140
147
|
const params = new URLSearchParams({ roleId: targetId })
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
148
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
149
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
143
150
|
const data = await readApiResultOrThrow<RoleResponse>(
|
|
144
151
|
`/api/dashboards/roles/widgets?${params.toString()}`,
|
|
145
152
|
undefined,
|
|
@@ -151,12 +158,12 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
151
158
|
setMode('override')
|
|
152
159
|
setOriginalMode('override')
|
|
153
160
|
setEffective(ids)
|
|
154
|
-
}, [
|
|
161
|
+
}, [targetId])
|
|
155
162
|
|
|
156
|
-
const loadUserData = React.useCallback(async () => {
|
|
163
|
+
const loadUserData = React.useCallback(async (forTenantId: string | null | undefined, forOrganizationId: string | null | undefined) => {
|
|
157
164
|
const params = new URLSearchParams({ userId: targetId })
|
|
158
|
-
if (
|
|
159
|
-
if (
|
|
165
|
+
if (forTenantId) params.set('tenantId', forTenantId)
|
|
166
|
+
if (forOrganizationId) params.set('organizationId', forOrganizationId)
|
|
160
167
|
const data = await readApiResultOrThrow<UserResponse>(
|
|
161
168
|
`/api/dashboards/users/widgets?${params.toString()}`,
|
|
162
169
|
undefined,
|
|
@@ -168,7 +175,7 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
168
175
|
setMode(data.mode || 'inherit')
|
|
169
176
|
setOriginalMode(data.mode || 'inherit')
|
|
170
177
|
setEffective(Array.isArray(data.effectiveWidgetIds) ? data.effectiveWidgetIds : [])
|
|
171
|
-
}, [
|
|
178
|
+
}, [targetId])
|
|
172
179
|
|
|
173
180
|
React.useEffect(() => {
|
|
174
181
|
let cancelled = false
|
|
@@ -177,21 +184,40 @@ export const WidgetVisibilityEditor = React.forwardRef<WidgetVisibilityEditorHan
|
|
|
177
184
|
setError(null)
|
|
178
185
|
try {
|
|
179
186
|
await loadCatalog()
|
|
180
|
-
if (kind === 'role') await loadRoleData()
|
|
181
|
-
else await loadUserData()
|
|
187
|
+
if (kind === 'role') await loadRoleData(tenantIdRef.current, organizationIdRef.current)
|
|
188
|
+
else await loadUserData(tenantIdRef.current, organizationIdRef.current)
|
|
182
189
|
} catch (err) {
|
|
183
190
|
console.error('Failed to load widget visibility data', err)
|
|
184
191
|
if (!cancelled) {
|
|
185
192
|
setError(tRef.current('dashboards.widgets.error.load', 'Unable to load widget configuration.'))
|
|
186
193
|
}
|
|
187
194
|
} finally {
|
|
188
|
-
if (!cancelled)
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setLoading(false)
|
|
197
|
+
hasMountedRef.current = true
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
200
|
}
|
|
191
201
|
load()
|
|
192
202
|
return () => { cancelled = true }
|
|
193
203
|
}, [kind, loadCatalog, loadRoleData, loadUserData])
|
|
194
204
|
|
|
205
|
+
React.useEffect(() => {
|
|
206
|
+
if (!hasMountedRef.current) return
|
|
207
|
+
if (preserveOnTenantChange) return
|
|
208
|
+
let cancelled = false
|
|
209
|
+
async function refetch() {
|
|
210
|
+
try {
|
|
211
|
+
if (kind === 'role') await loadRoleData(tenantId, organizationId)
|
|
212
|
+
else await loadUserData(tenantId, organizationId)
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error('Failed to reload widget visibility data', err)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
refetch()
|
|
218
|
+
return () => { cancelled = true }
|
|
219
|
+
}, [tenantId, organizationId, kind, loadRoleData, loadUserData, preserveOnTenantChange])
|
|
220
|
+
|
|
195
221
|
const toggle = React.useCallback((id: string) => {
|
|
196
222
|
setSelected((prev) => (prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id]))
|
|
197
223
|
}, [])
|
|
@@ -44,6 +44,8 @@ export async function GET(req: Request) {
|
|
|
44
44
|
description: integration.description ?? null,
|
|
45
45
|
providerKey: integration.providerKey ?? null,
|
|
46
46
|
direction: adapter.direction,
|
|
47
|
+
runMode: adapter.runMode ?? 'generic',
|
|
48
|
+
canStartRun: adapter.runMode !== 'provider',
|
|
47
49
|
supportedEntities: adapter.supportedEntities,
|
|
48
50
|
hasCredentials: Boolean(credentials),
|
|
49
51
|
isEnabled: state.isEnabled,
|
|
@@ -20,76 +20,95 @@ export const openApi = {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export async function POST(req: Request) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const scope = {
|
|
41
|
+
organizationId: auth.orgId as string,
|
|
42
|
+
tenantId: auth.tenantId,
|
|
43
|
+
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|