@open-mercato/core 0.6.6-develop.6176.1.4507b99c2f → 0.6.6-develop.6198.1.0822f97cc6
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/frontend/login.js +5 -5
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +26 -14
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +169 -149
- package/dist/modules/customers/api/deals/[id]/route.js.map +3 -3
- package/dist/modules/customers/components/detail/InlineEditors.js +9 -4
- package/dist/modules/customers/components/detail/InlineEditors.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +13 -9
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js +17 -0
- package/dist/modules/integrations/backend/integrations/detail-page-refresh.js.map +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +299 -297
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialog.js +120 -141
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +114 -118
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +155 -260
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/frontend/login.tsx +5 -5
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +4 -3
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +27 -17
- package/src/modules/customers/api/deals/[id]/route.ts +208 -174
- package/src/modules/customers/components/detail/InlineEditors.tsx +6 -10
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +13 -9
- package/src/modules/integrations/backend/integrations/detail-page-refresh.ts +36 -0
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +351 -327
- package/src/modules/workflows/components/EdgeEditDialog.tsx +132 -157
- package/src/modules/workflows/components/NodeEditDialog.tsx +116 -121
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +172 -239
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6198.1.0822f97cc6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -246,16 +246,16 @@
|
|
|
246
246
|
"zod": "^4.4.3"
|
|
247
247
|
},
|
|
248
248
|
"peerDependencies": {
|
|
249
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
250
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
251
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
249
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6198.1.0822f97cc6",
|
|
250
|
+
"@open-mercato/shared": "0.6.6-develop.6198.1.0822f97cc6",
|
|
251
|
+
"@open-mercato/ui": "0.6.6-develop.6198.1.0822f97cc6",
|
|
252
252
|
"react": "^19.0.0",
|
|
253
253
|
"react-dom": "^19.0.0"
|
|
254
254
|
},
|
|
255
255
|
"devDependencies": {
|
|
256
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
257
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
258
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
256
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6198.1.0822f97cc6",
|
|
257
|
+
"@open-mercato/shared": "0.6.6-develop.6198.1.0822f97cc6",
|
|
258
|
+
"@open-mercato/ui": "0.6.6-develop.6198.1.0822f97cc6",
|
|
259
259
|
"@testing-library/dom": "^10.4.1",
|
|
260
260
|
"@testing-library/jest-dom": "^6.9.1",
|
|
261
261
|
"@testing-library/react": "^16.3.1",
|
|
@@ -383,15 +383,15 @@ export default function LoginPage() {
|
|
|
383
383
|
</div>
|
|
384
384
|
) : null}
|
|
385
385
|
{showTenantInvalid ? (
|
|
386
|
-
<div className="rounded-md border border-
|
|
386
|
+
<div className="rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-xs text-status-error-text">
|
|
387
387
|
<div className="font-medium">{translate('auth.login.errors.tenantInvalid', 'Tenant not found. Clear the tenant selection and try again.')}</div>
|
|
388
|
-
<Button type="button" variant="outline" size="sm" className="mt-2 border-
|
|
388
|
+
<Button type="button" variant="outline" size="sm" className="mt-2 border-status-error-border text-status-error-text hover:text-status-error-text" onClick={handleClearTenant}>
|
|
389
389
|
<X className="mr-2 size-4" aria-hidden="true" />
|
|
390
390
|
{translate('auth.login.tenantClear', 'Clear')}
|
|
391
391
|
</Button>
|
|
392
392
|
</div>
|
|
393
393
|
) : tenantId ? (
|
|
394
|
-
<div className="rounded-md border border-
|
|
394
|
+
<div className="rounded-md border border-status-success-border bg-status-success-bg px-3 py-2 text-center text-xs text-status-success-text">
|
|
395
395
|
<div className="font-medium">
|
|
396
396
|
{tenantLoading
|
|
397
397
|
? translate('auth.login.tenantLoading', 'Loading tenant details...')
|
|
@@ -399,14 +399,14 @@ export default function LoginPage() {
|
|
|
399
399
|
tenant: tenantName || tenantId,
|
|
400
400
|
})}
|
|
401
401
|
</div>
|
|
402
|
-
<Button type="button" variant="outline" size="sm" className="mt-2 border-
|
|
402
|
+
<Button type="button" variant="outline" size="sm" className="mt-2 border-status-success-border text-status-success-text hover:text-status-success-text" onClick={handleClearTenant}>
|
|
403
403
|
<X className="mr-2 size-4" aria-hidden="true" />
|
|
404
404
|
{translate('auth.login.tenantClear', 'Clear')}
|
|
405
405
|
</Button>
|
|
406
406
|
</div>
|
|
407
407
|
) : null}
|
|
408
408
|
{error && !showTenantInvalid && (
|
|
409
|
-
<div className="rounded-md border border-
|
|
409
|
+
<div className="rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-center text-sm text-status-error-text" role="alert" aria-live="polite">
|
|
410
410
|
{error}
|
|
411
411
|
</div>
|
|
412
412
|
)}
|
|
@@ -428,12 +428,13 @@ export function MerchandisingAssistantSheet({
|
|
|
428
428
|
</div>
|
|
429
429
|
<div className="flex flex-col gap-0.5">
|
|
430
430
|
{agents.map((agent) => (
|
|
431
|
-
<
|
|
431
|
+
<Button
|
|
432
432
|
key={agent.id}
|
|
433
433
|
type="button"
|
|
434
|
+
variant="ghost"
|
|
434
435
|
onClick={() => handleSelectAgent(agent.id)}
|
|
435
436
|
data-ai-merchandising-agent-option={agent.id}
|
|
436
|
-
className="
|
|
437
|
+
className="h-auto w-full items-start justify-start gap-2 whitespace-normal rounded-sm px-2 py-2 text-left text-sm font-normal"
|
|
437
438
|
>
|
|
438
439
|
<span className="mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
|
|
439
440
|
{agent.icon}
|
|
@@ -444,7 +445,7 @@ export function MerchandisingAssistantSheet({
|
|
|
444
445
|
{agent.description}
|
|
445
446
|
</span>
|
|
446
447
|
</span>
|
|
447
|
-
</
|
|
448
|
+
</Button>
|
|
448
449
|
))}
|
|
449
450
|
</div>
|
|
450
451
|
</PopoverContent>
|
|
@@ -3,6 +3,8 @@ import * as React from 'react'
|
|
|
3
3
|
import type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'
|
|
4
4
|
import { subscribeProductSeoValidation } from './state'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
|
|
7
|
+
import { Alert } from '@open-mercato/ui/primitives/alert'
|
|
6
8
|
|
|
7
9
|
type SeoData = {
|
|
8
10
|
title?: string | null
|
|
@@ -14,6 +16,8 @@ type ValidationState = { ok: boolean; issues: string[]; message?: string }
|
|
|
14
16
|
|
|
15
17
|
type IssueKey = 'addTitle' | 'titleTooShort' | 'titleTooLong' | 'addDescription' | 'descriptionTooShort'
|
|
16
18
|
|
|
19
|
+
type SeoScore = { text: string; variant: StatusBadgeVariant }
|
|
20
|
+
|
|
17
21
|
function computeIssueKeys(title: string, description: string): IssueKey[] {
|
|
18
22
|
const issues: IssueKey[] = []
|
|
19
23
|
if (!title) {
|
|
@@ -51,27 +55,27 @@ export default function ProductSeoWidget({ data }: InjectionWidgetComponentProps
|
|
|
51
55
|
})
|
|
52
56
|
}, [])
|
|
53
57
|
|
|
54
|
-
const titleScore = React.useMemo(() => {
|
|
55
|
-
if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'),
|
|
56
|
-
if (title.length < 10) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'),
|
|
57
|
-
if (title.length > 60) return { text: t('catalog.products.create.seoWidget.tooLong', 'Too long'),
|
|
58
|
-
return { text: t('catalog.products.create.seoWidget.good', 'Good'),
|
|
58
|
+
const titleScore = React.useMemo<SeoScore>(() => {
|
|
59
|
+
if (!title) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }
|
|
60
|
+
if (title.length < 10) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }
|
|
61
|
+
if (title.length > 60) return { text: t('catalog.products.create.seoWidget.tooLong', 'Too long'), variant: 'warning' }
|
|
62
|
+
return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }
|
|
59
63
|
}, [title, t])
|
|
60
64
|
|
|
61
|
-
const descScore = React.useMemo(() => {
|
|
62
|
-
if (!description) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'),
|
|
63
|
-
if (description.length < 50) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'),
|
|
64
|
-
return { text: t('catalog.products.create.seoWidget.good', 'Good'),
|
|
65
|
+
const descScore = React.useMemo<SeoScore>(() => {
|
|
66
|
+
if (!description) return { text: t('catalog.products.create.seoWidget.missing', 'Missing'), variant: 'error' }
|
|
67
|
+
if (description.length < 50) return { text: t('catalog.products.create.seoWidget.tooShort', 'Too short'), variant: 'warning' }
|
|
68
|
+
return { text: t('catalog.products.create.seoWidget.good', 'Good'), variant: 'success' }
|
|
65
69
|
}, [description, t])
|
|
66
70
|
|
|
67
71
|
const statusBadge = validation.ok ? (
|
|
68
|
-
<
|
|
72
|
+
<StatusBadge variant="success">
|
|
69
73
|
{t('catalog.products.create.seoWidget.ready', 'Ready')}
|
|
70
|
-
</
|
|
74
|
+
</StatusBadge>
|
|
71
75
|
) : (
|
|
72
|
-
<
|
|
76
|
+
<StatusBadge variant="warning">
|
|
73
77
|
{t('catalog.products.create.seoWidget.needsAttention', 'Needs attention')}
|
|
74
|
-
</
|
|
78
|
+
</StatusBadge>
|
|
75
79
|
)
|
|
76
80
|
|
|
77
81
|
const translateIssue = (issueKey: string): string => {
|
|
@@ -89,7 +93,13 @@ export default function ProductSeoWidget({ data }: InjectionWidgetComponentProps
|
|
|
89
93
|
</div>
|
|
90
94
|
|
|
91
95
|
{validation.message || validation.issues.length ? (
|
|
92
|
-
<
|
|
96
|
+
<Alert
|
|
97
|
+
status={validation.ok ? 'success' : 'warning'}
|
|
98
|
+
style="lighter"
|
|
99
|
+
size="sm"
|
|
100
|
+
showIcon={false}
|
|
101
|
+
className="text-xs"
|
|
102
|
+
>
|
|
93
103
|
{validation.message ? <div className="font-medium">{validation.message}</div> : null}
|
|
94
104
|
{validation.issues.length ? (
|
|
95
105
|
<ul className="ml-4 list-disc space-y-1 pt-1">
|
|
@@ -98,17 +108,17 @@ export default function ProductSeoWidget({ data }: InjectionWidgetComponentProps
|
|
|
98
108
|
))}
|
|
99
109
|
</ul>
|
|
100
110
|
) : null}
|
|
101
|
-
</
|
|
111
|
+
</Alert>
|
|
102
112
|
) : null}
|
|
103
113
|
|
|
104
114
|
<div className="rounded border bg-muted/30 p-3 text-sm">
|
|
105
115
|
<div className="flex items-center justify-between">
|
|
106
116
|
<span className="text-muted-foreground">{t('catalog.products.create.seoWidget.titleLabel', 'Title ({{count}} chars)', { count: title.length })}</span>
|
|
107
|
-
<
|
|
117
|
+
<StatusBadge variant={titleScore.variant}>{titleScore.text}</StatusBadge>
|
|
108
118
|
</div>
|
|
109
119
|
<div className="mt-2 flex items-center justify-between">
|
|
110
120
|
<span className="text-muted-foreground">{t('catalog.products.create.seoWidget.descriptionLabel', 'Description ({{count}} chars)', { count: description.length })}</span>
|
|
111
|
-
<
|
|
121
|
+
<StatusBadge variant={descScore.variant}>{descScore.text}</StatusBadge>
|
|
112
122
|
</div>
|
|
113
123
|
</div>
|
|
114
124
|
|
|
@@ -386,103 +386,119 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
386
386
|
tenantId: deal.tenantId ?? auth.tenantId ?? null,
|
|
387
387
|
organizationId: deal.organizationId ?? auth.orgId ?? null,
|
|
388
388
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
{ deal: deal.id },
|
|
406
|
-
{ orderBy: { createdAt: 'ASC' } },
|
|
407
|
-
decryptionScope,
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
linkedPersonIds = Array.from(
|
|
411
|
-
new Set(
|
|
412
|
-
personLinkRows
|
|
413
|
-
.map((link) => {
|
|
414
|
-
const personRef = link.person
|
|
415
|
-
if (!personRef) return null
|
|
416
|
-
if (typeof personRef === 'string') return personRef
|
|
417
|
-
const personIdValue = personRef.id
|
|
418
|
-
return typeof personIdValue === 'string' ? personIdValue : null
|
|
419
|
-
})
|
|
420
|
-
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0),
|
|
421
|
-
),
|
|
422
|
-
)
|
|
423
|
-
linkedCompanyIds = Array.from(
|
|
424
|
-
new Set(
|
|
425
|
-
companyLinkRows
|
|
426
|
-
.map((link) => {
|
|
427
|
-
const companyRef = link.company
|
|
428
|
-
if (!companyRef) return null
|
|
429
|
-
if (typeof companyRef === 'string') return companyRef
|
|
430
|
-
const companyIdValue = companyRef.id
|
|
431
|
-
return typeof companyIdValue === 'string' ? companyIdValue : null
|
|
432
|
-
})
|
|
433
|
-
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0),
|
|
434
|
-
),
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
const previewPeople = linkedPersonIds.length
|
|
438
|
-
? await findWithDecryption(
|
|
389
|
+
// Issue #3175: the reads below only depend on the already-resolved deal +
|
|
390
|
+
// organization scope + decryption scope, so they are grouped into Promise.all
|
|
391
|
+
// batches instead of being awaited one after another. True dependencies stay
|
|
392
|
+
// ordered: preview/decrypt run after their link rows, pipeline reads after the
|
|
393
|
+
// effective stage is known, and the appearance map / audit fallback after the
|
|
394
|
+
// pipeline stages load. This mirrors the parallel-read pattern already used
|
|
395
|
+
// across the customers list/interactions/activities routes.
|
|
396
|
+
const resolveAssociations = async (): Promise<{
|
|
397
|
+
people: DealAssociation[]
|
|
398
|
+
companies: DealAssociation[]
|
|
399
|
+
linkedPersonIds: string[]
|
|
400
|
+
linkedCompanyIds: string[]
|
|
401
|
+
}> => {
|
|
402
|
+
if (liteView) {
|
|
403
|
+
const [personLinkRows, companyLinkRows] = await Promise.all([
|
|
404
|
+
findWithDecryption(
|
|
439
405
|
em,
|
|
440
|
-
|
|
441
|
-
{
|
|
442
|
-
{
|
|
406
|
+
CustomerDealPersonLink,
|
|
407
|
+
{ deal: deal.id },
|
|
408
|
+
{ orderBy: { createdAt: 'ASC' } },
|
|
443
409
|
decryptionScope,
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
const previewCompanies = linkedCompanyIds.length
|
|
447
|
-
? await findWithDecryption(
|
|
410
|
+
),
|
|
411
|
+
findWithDecryption(
|
|
448
412
|
em,
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
{
|
|
413
|
+
CustomerDealCompanyLink,
|
|
414
|
+
{ deal: deal.id },
|
|
415
|
+
{ orderBy: { createdAt: 'ASC' } },
|
|
452
416
|
decryptionScope,
|
|
453
|
-
)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
417
|
+
),
|
|
418
|
+
])
|
|
419
|
+
|
|
420
|
+
const linkedPersonIds = Array.from(
|
|
421
|
+
new Set(
|
|
422
|
+
personLinkRows
|
|
423
|
+
.map((link) => {
|
|
424
|
+
const personRef = link.person
|
|
425
|
+
if (!personRef) return null
|
|
426
|
+
if (typeof personRef === 'string') return personRef
|
|
427
|
+
const personIdValue = personRef.id
|
|
428
|
+
return typeof personIdValue === 'string' ? personIdValue : null
|
|
429
|
+
})
|
|
430
|
+
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0),
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
const linkedCompanyIds = Array.from(
|
|
434
|
+
new Set(
|
|
435
|
+
companyLinkRows
|
|
436
|
+
.map((link) => {
|
|
437
|
+
const companyRef = link.company
|
|
438
|
+
if (!companyRef) return null
|
|
439
|
+
if (typeof companyRef === 'string') return companyRef
|
|
440
|
+
const companyIdValue = companyRef.id
|
|
441
|
+
return typeof companyIdValue === 'string' ? companyIdValue : null
|
|
442
|
+
})
|
|
443
|
+
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0),
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
const [previewPeople, previewCompanies] = await Promise.all([
|
|
448
|
+
linkedPersonIds.length
|
|
449
|
+
? findWithDecryption(
|
|
450
|
+
em,
|
|
451
|
+
CustomerEntity,
|
|
452
|
+
{ id: { $in: linkedPersonIds.slice(0, 3) } },
|
|
453
|
+
{ populate: ['personProfile'] },
|
|
454
|
+
decryptionScope,
|
|
455
|
+
)
|
|
456
|
+
: [],
|
|
457
|
+
linkedCompanyIds.length
|
|
458
|
+
? findWithDecryption(
|
|
459
|
+
em,
|
|
460
|
+
CustomerEntity,
|
|
461
|
+
{ id: { $in: linkedCompanyIds.slice(0, 3) } },
|
|
462
|
+
{ populate: ['companyProfile'] },
|
|
463
|
+
decryptionScope,
|
|
464
|
+
)
|
|
465
|
+
: [],
|
|
466
|
+
])
|
|
467
|
+
const previewPeopleMap = new Map(previewPeople.map((entity) => [entity.id, entity]))
|
|
468
|
+
const previewCompaniesMap = new Map(previewCompanies.map((entity) => [entity.id, entity]))
|
|
469
|
+
const people = linkedPersonIds.slice(0, 3).reduce<DealAssociation[]>((acc, personId) => {
|
|
470
|
+
const entity = previewPeopleMap.get(personId) ?? null
|
|
471
|
+
if (!entity || entity.deletedAt) return acc
|
|
472
|
+
const { label, subtitle } = normalizePersonAssociation(entity)
|
|
473
|
+
acc.push({ id: entity.id, label, subtitle, kind: 'person' })
|
|
474
|
+
return acc
|
|
475
|
+
}, [])
|
|
476
|
+
const companies = linkedCompanyIds.slice(0, 3).reduce<DealAssociation[]>((acc, companyId) => {
|
|
477
|
+
const entity = previewCompaniesMap.get(companyId) ?? null
|
|
478
|
+
if (!entity || entity.deletedAt) return acc
|
|
479
|
+
const { label, subtitle } = normalizeCompanyAssociation(entity)
|
|
480
|
+
acc.push({ id: entity.id, label, subtitle, kind: 'company' })
|
|
481
|
+
return acc
|
|
482
|
+
}, [])
|
|
483
|
+
return { people, companies, linkedPersonIds, linkedCompanyIds }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const [personLinks, companyLinks] = await Promise.all([
|
|
487
|
+
findWithDecryption(
|
|
488
|
+
em,
|
|
489
|
+
CustomerDealPersonLink,
|
|
490
|
+
{ deal: deal.id },
|
|
491
|
+
{ populate: ['person', 'person.personProfile'] },
|
|
492
|
+
decryptionScope,
|
|
493
|
+
),
|
|
494
|
+
findWithDecryption(
|
|
495
|
+
em,
|
|
496
|
+
CustomerDealCompanyLink,
|
|
497
|
+
{ deal: deal.id },
|
|
498
|
+
{ populate: ['company', 'company.companyProfile'] },
|
|
499
|
+
decryptionScope,
|
|
500
|
+
),
|
|
501
|
+
])
|
|
486
502
|
const fallbackTenantId = deal.tenantId ?? auth.tenantId ?? null
|
|
487
503
|
const fallbackOrgId = deal.organizationId ?? auth.orgId ?? null
|
|
488
504
|
await decryptEntitiesWithFallbackScope(personLinks, {
|
|
@@ -496,7 +512,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
496
512
|
organizationId: fallbackOrgId,
|
|
497
513
|
})
|
|
498
514
|
|
|
499
|
-
people = personLinks.reduce<DealAssociation[]>((acc, link) => {
|
|
515
|
+
const people = personLinks.reduce<DealAssociation[]>((acc, link) => {
|
|
500
516
|
const entity = link.person as CustomerEntity | null
|
|
501
517
|
if (!entity || entity.deletedAt) return acc
|
|
502
518
|
const { label, subtitle } = normalizePersonAssociation(entity)
|
|
@@ -504,55 +520,89 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
504
520
|
return acc
|
|
505
521
|
}, [])
|
|
506
522
|
|
|
507
|
-
companies = companyLinks.reduce<DealAssociation[]>((acc, link) => {
|
|
523
|
+
const companies = companyLinks.reduce<DealAssociation[]>((acc, link) => {
|
|
508
524
|
const entity = link.company as CustomerEntity | null
|
|
509
525
|
if (!entity || entity.deletedAt) return acc
|
|
510
526
|
const { label, subtitle } = normalizeCompanyAssociation(entity)
|
|
511
527
|
acc.push({ id: entity.id, label, subtitle, kind: 'company' })
|
|
512
528
|
return acc
|
|
513
529
|
}, [])
|
|
514
|
-
|
|
515
|
-
|
|
530
|
+
return {
|
|
531
|
+
people,
|
|
532
|
+
companies,
|
|
533
|
+
linkedPersonIds: people.map((entry) => entry.id),
|
|
534
|
+
linkedCompanyIds: companies.map((entry) => entry.id),
|
|
535
|
+
}
|
|
516
536
|
}
|
|
517
537
|
|
|
518
|
-
const customFieldValues = await loadCustomFieldValues({
|
|
519
|
-
em,
|
|
520
|
-
entityId: E.customers.customer_deal,
|
|
521
|
-
recordIds: [deal.id],
|
|
522
|
-
tenantIdByRecord: { [deal.id]: deal.tenantId ?? null },
|
|
523
|
-
organizationIdByRecord: { [deal.id]: deal.organizationId ?? null },
|
|
524
|
-
tenantFallbacks: [deal.tenantId ?? auth.tenantId ?? null].filter((value): value is string => !!value),
|
|
525
|
-
})
|
|
526
|
-
const customFields = normalizeCustomFieldResponse(customFieldValues[deal.id]) ?? {}
|
|
527
|
-
|
|
528
538
|
const viewerUserId = auth.isApiKey ? null : auth.sub ?? null
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const viewerScope = {
|
|
533
|
-
tenantId: auth.tenantId ?? null,
|
|
534
|
-
organizationId: auth.orgId ?? null,
|
|
539
|
+
const resolveViewer = async (): Promise<{ name: string | null; email: string | null }> => {
|
|
540
|
+
if (!viewerUserId) {
|
|
541
|
+
return { name: null, email: auth.email ?? null }
|
|
535
542
|
}
|
|
536
543
|
const viewer = await findOneWithDecryption(
|
|
537
544
|
em,
|
|
538
545
|
User,
|
|
539
546
|
{ id: viewerUserId, tenantId: auth.tenantId ?? null },
|
|
540
547
|
{},
|
|
541
|
-
|
|
548
|
+
{ tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },
|
|
542
549
|
)
|
|
543
|
-
|
|
544
|
-
|
|
550
|
+
return {
|
|
551
|
+
name: viewer?.name ?? null,
|
|
552
|
+
email: viewer?.email ?? auth.email ?? null,
|
|
553
|
+
}
|
|
545
554
|
}
|
|
546
555
|
|
|
547
|
-
const
|
|
548
|
-
|
|
556
|
+
const resolveOwner = async () =>
|
|
557
|
+
deal.ownerUserId
|
|
558
|
+
? await findOneWithDecryption(
|
|
559
|
+
em,
|
|
560
|
+
User,
|
|
561
|
+
{ id: deal.ownerUserId, tenantId: deal.tenantId ?? auth.tenantId ?? null },
|
|
562
|
+
{},
|
|
563
|
+
decryptionScope,
|
|
564
|
+
)
|
|
565
|
+
: null
|
|
566
|
+
|
|
567
|
+
const resolveStageTransitions = async (): Promise<CustomerDealStageTransition[]> => {
|
|
568
|
+
if (!includeStages) return []
|
|
569
|
+
try {
|
|
570
|
+
return await findWithDecryption(
|
|
571
|
+
em,
|
|
572
|
+
CustomerDealStageTransition,
|
|
573
|
+
{ deal: deal.id, deletedAt: null },
|
|
574
|
+
{ orderBy: { stageOrder: 'ASC', transitionedAt: 'ASC' } },
|
|
575
|
+
decryptionScope,
|
|
576
|
+
)
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (!isMissingDealStageTransitionTable(error)) {
|
|
579
|
+
throw error
|
|
580
|
+
}
|
|
581
|
+
warnMissingDealStageTransitionTable('customers.api.deals.detail.GET')
|
|
582
|
+
return []
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const [associations, customFieldValues, viewerInfo, owner, effectiveStage, stageTransitions] = await Promise.all([
|
|
587
|
+
resolveAssociations(),
|
|
588
|
+
loadCustomFieldValues({
|
|
549
589
|
em,
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
{},
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
590
|
+
entityId: E.customers.customer_deal,
|
|
591
|
+
recordIds: [deal.id],
|
|
592
|
+
tenantIdByRecord: { [deal.id]: deal.tenantId ?? null },
|
|
593
|
+
organizationIdByRecord: { [deal.id]: deal.organizationId ?? null },
|
|
594
|
+
tenantFallbacks: [deal.tenantId ?? auth.tenantId ?? null].filter((value): value is string => !!value),
|
|
595
|
+
}),
|
|
596
|
+
resolveViewer(),
|
|
597
|
+
resolveOwner(),
|
|
598
|
+
includeStages ? resolveEffectivePipelineStage(em, deal, decryptionScope) : Promise.resolve(null),
|
|
599
|
+
resolveStageTransitions(),
|
|
600
|
+
])
|
|
601
|
+
|
|
602
|
+
const { people, companies, linkedPersonIds, linkedCompanyIds } = associations
|
|
603
|
+
const customFields = normalizeCustomFieldResponse(customFieldValues[deal.id]) ?? {}
|
|
604
|
+
const viewerName = viewerInfo.name
|
|
605
|
+
const viewerEmail = viewerInfo.email
|
|
556
606
|
const ownerPayload = owner
|
|
557
607
|
? {
|
|
558
608
|
id: owner.id,
|
|
@@ -561,69 +611,53 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
561
611
|
}
|
|
562
612
|
: null
|
|
563
613
|
|
|
564
|
-
const effectiveStage = includeStages
|
|
565
|
-
? await resolveEffectivePipelineStage(em, deal, decryptionScope)
|
|
566
|
-
: null
|
|
567
614
|
const effectivePipelineId = deal.pipelineId ?? effectiveStage?.pipelineId ?? null
|
|
568
615
|
const effectivePipelineStageId = deal.pipelineStageId ?? effectiveStage?.id ?? null
|
|
569
616
|
const effectivePipelineStageLabel = deal.pipelineStage ?? effectiveStage?.label ?? null
|
|
570
617
|
|
|
571
|
-
const pipelineStages =
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
CustomerPipelineStage,
|
|
575
|
-
{
|
|
576
|
-
pipelineId: effectivePipelineId,
|
|
577
|
-
organizationId: deal.organizationId,
|
|
578
|
-
tenantId: deal.tenantId,
|
|
579
|
-
},
|
|
580
|
-
{ orderBy: { order: 'ASC' } },
|
|
581
|
-
decryptionScope,
|
|
582
|
-
)
|
|
583
|
-
: []
|
|
584
|
-
const pipeline = effectivePipelineId
|
|
585
|
-
? await findOneWithDecryption(
|
|
586
|
-
em,
|
|
587
|
-
CustomerPipeline,
|
|
588
|
-
{
|
|
589
|
-
id: effectivePipelineId,
|
|
590
|
-
organizationId: deal.organizationId,
|
|
591
|
-
tenantId: deal.tenantId,
|
|
592
|
-
},
|
|
593
|
-
{},
|
|
594
|
-
decryptionScope,
|
|
595
|
-
)
|
|
596
|
-
: null
|
|
597
|
-
const pipelineStageAppearanceMap = pipelineStages.length
|
|
598
|
-
? await loadPipelineStageAppearanceMap(em, pipelineStages, deal.organizationId, deal.tenantId)
|
|
599
|
-
: new Map<string, CustomerDictionaryEntry>()
|
|
600
|
-
let stageTransitions: CustomerDealStageTransition[] = []
|
|
601
|
-
if (includeStages) {
|
|
602
|
-
try {
|
|
603
|
-
stageTransitions = await findWithDecryption(
|
|
618
|
+
const [pipelineStages, pipeline] = await Promise.all([
|
|
619
|
+
includeStages && effectivePipelineId
|
|
620
|
+
? findWithDecryption(
|
|
604
621
|
em,
|
|
605
|
-
|
|
606
|
-
{
|
|
607
|
-
|
|
622
|
+
CustomerPipelineStage,
|
|
623
|
+
{
|
|
624
|
+
pipelineId: effectivePipelineId,
|
|
625
|
+
organizationId: deal.organizationId,
|
|
626
|
+
tenantId: deal.tenantId,
|
|
627
|
+
},
|
|
628
|
+
{ orderBy: { order: 'ASC' } },
|
|
608
629
|
decryptionScope,
|
|
609
630
|
)
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
631
|
+
: [],
|
|
632
|
+
effectivePipelineId
|
|
633
|
+
? findOneWithDecryption(
|
|
634
|
+
em,
|
|
635
|
+
CustomerPipeline,
|
|
636
|
+
{
|
|
637
|
+
id: effectivePipelineId,
|
|
638
|
+
organizationId: deal.organizationId,
|
|
639
|
+
tenantId: deal.tenantId,
|
|
640
|
+
},
|
|
641
|
+
{},
|
|
642
|
+
decryptionScope,
|
|
643
|
+
)
|
|
644
|
+
: null,
|
|
645
|
+
])
|
|
646
|
+
|
|
618
647
|
const persistedStageTransitions = stageTransitions.map((transition) => ({
|
|
619
648
|
stageId: transition.stageId,
|
|
620
649
|
stageLabel: transition.stageLabel,
|
|
621
650
|
stageOrder: transition.stageOrder,
|
|
622
651
|
transitionedAt: transition.transitionedAt.toISOString(),
|
|
623
652
|
}))
|
|
624
|
-
const recoveredStageTransitions =
|
|
625
|
-
|
|
626
|
-
|
|
653
|
+
const [pipelineStageAppearanceMap, recoveredStageTransitions] = await Promise.all([
|
|
654
|
+
pipelineStages.length
|
|
655
|
+
? loadPipelineStageAppearanceMap(em, pipelineStages, deal.organizationId, deal.tenantId)
|
|
656
|
+
: new Map<string, CustomerDictionaryEntry>(),
|
|
657
|
+
includeStages && persistedStageTransitions.length === 0
|
|
658
|
+
? loadAuditStageTransitionsFallback({ container, deal, pipelineStages })
|
|
659
|
+
: [],
|
|
660
|
+
])
|
|
627
661
|
const effectiveCurrentStage = (() => {
|
|
628
662
|
if (!effectivePipelineStageId) return null
|
|
629
663
|
const matchingStage = pipelineStages.find((stage) => stage.id === effectivePipelineStageId)
|