@open-mercato/core 0.10.1-develop.7761.1.9dfaf470ed → 0.10.1-develop.7763.1.caea616ebf
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/profile/route.js +16 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/lib/userDataSubject.js +26 -0
- package/dist/modules/auth/lib/userDataSubject.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js +1 -0
- package/dist/modules/customer_accounts/widgets/injection/account-status/widget.client.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -2
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +11 -12
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/create/page.js +6 -0
- package/dist/modules/customers/backend/customers/companies/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +25 -25
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js +8 -27
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +175 -171
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js +25 -24
- package/dist/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/create/page.js +9 -0
- package/dist/modules/customers/backend/customers/people/create/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +140 -144
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js +2 -0
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +13 -4
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +13 -3
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/CustomerDetailSidebar.js +29 -0
- package/dist/modules/customers/components/detail/CustomerDetailSidebar.js.map +7 -0
- package/dist/modules/customers/components/detail/DealDetailHeader.js +4 -2
- package/dist/modules/customers/components/detail/DealDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +9 -1
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +5 -0
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailHeader.js +2 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +9 -1
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/create/CreateDealForm.js +160 -176
- package/dist/modules/customers/components/detail/create/CreateDealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/useCustomerInjectedTabs.js +47 -0
- package/dist/modules/customers/components/detail/useCustomerInjectedTabs.js.map +7 -0
- package/dist/modules/customers/components/formConfig.js +3 -0
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/extension-points.js +14 -4
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/detailResponseEnrichment.js +38 -0
- package/dist/modules/customers/lib/detailResponseEnrichment.js.map +7 -0
- package/dist/modules/dashboards/api/layout/[itemId]/route.js +11 -4
- package/dist/modules/dashboards/api/layout/[itemId]/route.js.map +2 -2
- package/dist/modules/dashboards/api/layout/route.js +25 -13
- package/dist/modules/dashboards/api/layout/route.js.map +2 -2
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js +4 -4
- package/dist/modules/integrations/widgets/injection/external-ids/widget.client.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/profile/route.ts +15 -1
- package/src/modules/auth/lib/userDataSubject.ts +30 -0
- package/src/modules/customer_accounts/widgets/injection/account-status/widget.client.tsx +2 -0
- package/src/modules/customers/api/companies/[id]/route.ts +3 -2
- package/src/modules/customers/api/deals/[id]/route.ts +12 -12
- package/src/modules/customers/api/people/[id]/route.ts +3 -2
- package/src/modules/customers/backend/customers/companies/create/page.tsx +6 -0
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +27 -30
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealInjectedTabs.tsx +9 -44
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +7 -3
- package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx +25 -27
- package/src/modules/customers/backend/customers/people/create/page.tsx +9 -0
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +21 -33
- package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +3 -0
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +15 -4
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +15 -4
- package/src/modules/customers/components/detail/CustomerDetailSidebar.tsx +41 -0
- package/src/modules/customers/components/detail/DealDetailHeader.tsx +4 -1
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +11 -1
- package/src/modules/customers/components/detail/DealForm.tsx +5 -0
- package/src/modules/customers/components/detail/PersonDetailHeader.tsx +3 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +11 -1
- package/src/modules/customers/components/detail/create/CreateDealForm.tsx +126 -209
- package/src/modules/customers/components/detail/useCustomerInjectedTabs.tsx +64 -0
- package/src/modules/customers/components/formConfig.tsx +3 -0
- package/src/modules/customers/extension-points.ts +14 -4
- package/src/modules/customers/lib/detailResponseEnrichment.ts +53 -0
- package/src/modules/dashboards/api/layout/[itemId]/route.ts +12 -4
- package/src/modules/dashboards/api/layout/route.ts +25 -12
- package/src/modules/integrations/widgets/injection/external-ids/widget.client.tsx +4 -4
|
@@ -14,6 +14,7 @@ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
14
14
|
import { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
15
15
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
16
16
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
17
|
+
import { resolveUserDataSubject } from '@open-mercato/core/modules/auth/lib/userDataSubject'
|
|
17
18
|
|
|
18
19
|
const logger = createLogger('auth').child({ component: 'profile' })
|
|
19
20
|
|
|
@@ -98,10 +99,18 @@ export async function GET(req: Request) {
|
|
|
98
99
|
try {
|
|
99
100
|
const container = await createRequestContainer()
|
|
100
101
|
const em = (container.resolve('em') as EntityManager)
|
|
102
|
+
const userId = await resolveUserDataSubject(em, auth)
|
|
103
|
+
if (!userId) {
|
|
104
|
+
return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
|
|
105
|
+
}
|
|
101
106
|
const user = await findOneWithDecryption(
|
|
102
107
|
em,
|
|
103
108
|
User,
|
|
104
|
-
{
|
|
109
|
+
{
|
|
110
|
+
id: userId,
|
|
111
|
+
deletedAt: null,
|
|
112
|
+
...(auth.isApiKey ? { tenantId: auth.tenantId, ...(auth.orgId ? { organizationId: auth.orgId } : {}) } : {}),
|
|
113
|
+
},
|
|
105
114
|
undefined,
|
|
106
115
|
{ tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },
|
|
107
116
|
)
|
|
@@ -126,6 +135,9 @@ export async function PUT(req: Request) {
|
|
|
126
135
|
if (!auth?.sub) {
|
|
127
136
|
return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })
|
|
128
137
|
}
|
|
138
|
+
if (auth.isApiKey) {
|
|
139
|
+
return NextResponse.json({ error: translate('api.errors.forbidden', 'Forbidden') }, { status: 403 })
|
|
140
|
+
}
|
|
129
141
|
try {
|
|
130
142
|
const body = await req.json().catch(() => ({}))
|
|
131
143
|
const parsed = buildUpdateSchema(translate).safeParse(body)
|
|
@@ -222,6 +234,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
222
234
|
responses: [
|
|
223
235
|
{ status: 200, description: 'Profile payload', schema: profileResponseSchema },
|
|
224
236
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
237
|
+
{ status: 403, description: 'No user binding in the authenticated scope', schema: z.object({ error: z.string() }) },
|
|
225
238
|
{ status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },
|
|
226
239
|
],
|
|
227
240
|
},
|
|
@@ -236,6 +249,7 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
236
249
|
{ status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },
|
|
237
250
|
{ status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },
|
|
238
251
|
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
252
|
+
{ status: 403, description: 'Interactive session required', schema: z.object({ error: z.string() }) },
|
|
239
253
|
],
|
|
240
254
|
},
|
|
241
255
|
},
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
|
+
import { User } from '../data/entities'
|
|
6
|
+
|
|
7
|
+
const uuidSchema = z.string().uuid()
|
|
8
|
+
|
|
9
|
+
export async function resolveUserDataSubject(
|
|
10
|
+
em: EntityManager,
|
|
11
|
+
auth: NonNullable<AuthContext>,
|
|
12
|
+
): Promise<string | null> {
|
|
13
|
+
if (!auth.isApiKey) return auth.sub
|
|
14
|
+
if (!uuidSchema.safeParse(auth.userId).success || !uuidSchema.safeParse(auth.tenantId).success) return null
|
|
15
|
+
if (auth.orgId !== null && !uuidSchema.safeParse(auth.orgId).success) return null
|
|
16
|
+
|
|
17
|
+
const user = await findOneWithDecryption(
|
|
18
|
+
em,
|
|
19
|
+
User,
|
|
20
|
+
{
|
|
21
|
+
id: auth.userId,
|
|
22
|
+
tenantId: auth.tenantId,
|
|
23
|
+
...(auth.orgId ? { organizationId: auth.orgId } : {}),
|
|
24
|
+
deletedAt: null,
|
|
25
|
+
},
|
|
26
|
+
undefined,
|
|
27
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
28
|
+
)
|
|
29
|
+
return user?.id ?? null
|
|
30
|
+
}
|
|
@@ -325,6 +325,8 @@ export default function AccountStatusWidget({ context }: AccountStatusProps) {
|
|
|
325
325
|
queryClient.invalidateQueries({ queryKey: ['customer-account-pending-invitation', personEntityId] })
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
if (!personEntityId) return null
|
|
329
|
+
|
|
328
330
|
if (isLoading) {
|
|
329
331
|
return <div className="text-sm text-muted-foreground">{t('common.loading', 'Loading...')}</div>
|
|
330
332
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enrichCustomerDetailResponse } from '../../../lib/detailResponseEnrichment'
|
|
1
2
|
import { NextResponse } from 'next/server'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
@@ -524,7 +525,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
524
525
|
: null
|
|
525
526
|
if (cache && cacheKey) {
|
|
526
527
|
const cached = await runWithCacheTenant(cacheTenantId, () => cache.get(cacheKey))
|
|
527
|
-
if (cached) return NextResponse.json(cached)
|
|
528
|
+
if (cached) return NextResponse.json(await enrichCustomerDetailResponse(cached as Record<string, unknown>, 'company', { auth, container, em, tenantId: companyScope.tenantId, organizationId: companyScope.organizationId }))
|
|
528
529
|
}
|
|
529
530
|
|
|
530
531
|
const shouldLoadCanonicalInteractions = includeInteractions || includeActivities || includeTodos
|
|
@@ -1211,7 +1212,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
1211
1212
|
}
|
|
1212
1213
|
}
|
|
1213
1214
|
|
|
1214
|
-
return NextResponse.json(payload)
|
|
1215
|
+
return NextResponse.json(await enrichCustomerDetailResponse(payload, 'company', { auth, container, em, tenantId: companyScope.tenantId, organizationId: companyScope.organizationId }))
|
|
1215
1216
|
}
|
|
1216
1217
|
|
|
1217
1218
|
const companyDetailQuerySchema = z.object({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enrichCustomerDetailResponse } from '../../../lib/detailResponseEnrichment'
|
|
1
2
|
import { NextResponse } from 'next/server'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
@@ -491,20 +492,11 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
491
492
|
})
|
|
492
493
|
: null
|
|
493
494
|
|
|
495
|
+
let cachedBody: Record<string, unknown> | null = null
|
|
494
496
|
if (detailCache && detailCacheKey) {
|
|
495
497
|
try {
|
|
496
498
|
const cached = await runWithCacheTenant(cacheTenantId, () => detailCache.get(detailCacheKey))
|
|
497
|
-
if (cached && typeof cached === 'object')
|
|
498
|
-
const viewerInfo = await resolveViewer()
|
|
499
|
-
return NextResponse.json({
|
|
500
|
-
...(cached as Record<string, unknown>),
|
|
501
|
-
viewer: {
|
|
502
|
-
userId: viewerUserId,
|
|
503
|
-
name: viewerInfo.name,
|
|
504
|
-
email: viewerInfo.email,
|
|
505
|
-
},
|
|
506
|
-
})
|
|
507
|
-
}
|
|
499
|
+
if (cached && typeof cached === 'object') cachedBody = cached as Record<string, unknown>
|
|
508
500
|
} catch (err) {
|
|
509
501
|
// A cache-backend read error must degrade to a fresh DB read, never a 500.
|
|
510
502
|
debugCrudCache('get', {
|
|
@@ -514,6 +506,14 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
514
506
|
})
|
|
515
507
|
}
|
|
516
508
|
}
|
|
509
|
+
if (cachedBody) {
|
|
510
|
+
const viewerInfo = await resolveViewer()
|
|
511
|
+
return NextResponse.json(await enrichCustomerDetailResponse({
|
|
512
|
+
...cachedBody,
|
|
513
|
+
viewer: { userId: viewerUserId, name: viewerInfo.name, email: viewerInfo.email },
|
|
514
|
+
}, 'deal', { auth, container, em, tenantId: cacheTenantId, organizationId: cacheOrganizationId }))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
517
|
// Issue #3175: the reads below only depend on the already-resolved deal +
|
|
518
518
|
// organization scope + decryption scope, so they are grouped into Promise.all
|
|
519
519
|
// batches instead of being awaited one after another. True dependencies stay
|
|
@@ -872,7 +872,7 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
-
return NextResponse.json({ ...cacheableBody, viewer })
|
|
875
|
+
return NextResponse.json(await enrichCustomerDetailResponse({ ...cacheableBody, viewer }, 'deal', { auth, container, em, tenantId: cacheTenantId, organizationId: cacheOrganizationId }))
|
|
876
876
|
}
|
|
877
877
|
|
|
878
878
|
const dealDetailQuerySchema = z.object({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enrichCustomerDetailResponse } from '../../../lib/detailResponseEnrichment'
|
|
1
2
|
import { NextResponse } from 'next/server'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
@@ -579,7 +580,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
579
580
|
statusCode = 200
|
|
580
581
|
profileMeta = { cacheHit: true, include: Array.from(includeTokens), interactionMode }
|
|
581
582
|
profiler.mark('cache_hit')
|
|
582
|
-
return NextResponse.json(cached)
|
|
583
|
+
return NextResponse.json(await enrichCustomerDetailResponse(cached as Record<string, unknown>, 'person', { auth, container, em, tenantId: personDetailTenantId, organizationId: personDetailOrganizationId }))
|
|
583
584
|
}
|
|
584
585
|
}
|
|
585
586
|
|
|
@@ -1134,7 +1135,7 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
1134
1135
|
} catch {}
|
|
1135
1136
|
}
|
|
1136
1137
|
|
|
1137
|
-
const response = NextResponse.json(payload)
|
|
1138
|
+
const response = NextResponse.json(await enrichCustomerDetailResponse(payload, 'person', { auth, container, em, tenantId: personDetailTenantId, organizationId: personDetailOrganizationId }))
|
|
1138
1139
|
statusCode = 200
|
|
1139
1140
|
profileMeta = {
|
|
1140
1141
|
include: Array.from(includeTokens),
|
|
@@ -45,6 +45,8 @@ export default function CreateCompanyPage() {
|
|
|
45
45
|
initialValues={{ addresses: [] as CompanyFormValues['addresses'] }}
|
|
46
46
|
injectionSpotId={extensionPoints.hosts.companyForm.spotId}
|
|
47
47
|
legacyInjectionSpotId={COMPANY_CREATE_LEGACY_INJECTION_SPOT_ID}
|
|
48
|
+
entityId="customers.company"
|
|
49
|
+
resourceKind="customers.company"
|
|
48
50
|
entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
|
|
49
51
|
submitLabel={t('customers.companies.form.submit')}
|
|
50
52
|
cancelHref={returnTo ?? '/backend/customers/companies'}
|
|
@@ -125,6 +127,10 @@ export default function CreateCompanyPage() {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
flash(t('customers.companies.form.success'), 'success')
|
|
130
|
+
return newId ? { resourceId: newId } : undefined
|
|
131
|
+
}}
|
|
132
|
+
onSubmitSuccess={async (_values, result) => {
|
|
133
|
+
const newId = result?.resourceId
|
|
128
134
|
if (returnTo) router.push(returnTo)
|
|
129
135
|
else if (newId) router.push(`/backend/customers/companies-v2/${newId}`)
|
|
130
136
|
else router.push('/backend/customers/companies')
|
|
@@ -15,7 +15,9 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
15
15
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
16
16
|
import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState, type SectionAction } from '@open-mercato/ui/backend/detail'
|
|
17
17
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
18
|
-
import { InjectionSpot
|
|
18
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
19
|
+
import { useCustomerInjectedTabs } from '../../../../components/detail/useCustomerInjectedTabs'
|
|
20
|
+
import { CustomerDetailSidebar } from '../../../../components/detail/CustomerDetailSidebar'
|
|
19
21
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
20
22
|
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
21
23
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
@@ -32,7 +34,7 @@ import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
|
32
34
|
import { coerceDisplayName } from '../../../../lib/displayName'
|
|
33
35
|
import { isDetailNotFoundStatus } from '@open-mercato/core/modules/customers/lib/detailHelpers'
|
|
34
36
|
import { CompanyDetailHeader } from '../../../../components/detail/CompanyDetailHeader'
|
|
35
|
-
import { CompanyDetailTabs, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
37
|
+
import { CompanyDetailTabs, COMPANY_DETAIL_TAB_IDS, resolveLegacyTab, type CompanyTabId } from '../../../../components/detail/CompanyDetailTabs'
|
|
36
38
|
import { useDealsAccess } from '../../../../components/detail/useDealsAccess'
|
|
37
39
|
import { CompanyKpiBar } from '../../../../components/detail/CompanyKpiBar'
|
|
38
40
|
import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
|
|
@@ -192,9 +194,11 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
192
194
|
resourceKind: 'customers.company',
|
|
193
195
|
resourceId: currentCompanyId ?? (id ?? undefined),
|
|
194
196
|
data,
|
|
197
|
+
isDirty,
|
|
198
|
+
isSaving,
|
|
195
199
|
retryLastMutation,
|
|
196
200
|
}),
|
|
197
|
-
[currentCompanyId, data, id, mutationContextId, retryLastMutation],
|
|
201
|
+
[currentCompanyId, data, isDirty, isSaving, id, mutationContextId, retryLastMutation],
|
|
198
202
|
)
|
|
199
203
|
const runMutationWithContext = React.useCallback(
|
|
200
204
|
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
@@ -295,36 +299,19 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
295
299
|
setScheduleDialogOpen(true)
|
|
296
300
|
}, [])
|
|
297
301
|
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
const { injectedTabs, injectedTabMap, loading: injectedTabsLoading } = useCustomerInjectedTabs({
|
|
303
|
+
spotId: extensionPoints.hosts.companyTabs.spotId,
|
|
300
304
|
context: injectionContext,
|
|
301
|
-
|
|
305
|
+
data,
|
|
306
|
+
onDataChange: setData,
|
|
307
|
+
nativeTabIds: COMPANY_DETAIL_TAB_IDS,
|
|
302
308
|
})
|
|
303
309
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const tabId = widget.placement?.groupId ?? widget.widgetId
|
|
310
|
-
const label = widget.placement?.groupLabel
|
|
311
|
-
? t(widget.placement.groupLabel, widget.placement.groupLabel)
|
|
312
|
-
: widget.module.metadata.title ?? tabId
|
|
313
|
-
const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
|
|
314
|
-
const render = () => (
|
|
315
|
-
<widget.module.Widget
|
|
316
|
-
context={injectionContext}
|
|
317
|
-
data={data}
|
|
318
|
-
onDataChange={(next: unknown) => setData(next as CompanyOverview)}
|
|
319
|
-
/>
|
|
320
|
-
)
|
|
321
|
-
return { id: tabId, label, priority, render }
|
|
322
|
-
})
|
|
323
|
-
.sort((a, b) => b.priority - a.priority),
|
|
324
|
-
[data, injectedTabWidgets, injectionContext, t],
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
const injectedTabMap = React.useMemo(() => new Map(injectedTabs.map((tab) => [tab.id, tab.render])), [injectedTabs])
|
|
310
|
+
const injectedTabIds = React.useMemo(() => injectedTabs.map((tab) => tab.id), [injectedTabs])
|
|
311
|
+
React.useEffect(() => {
|
|
312
|
+
const requested = resolveLegacyTab(searchParams?.get('tab'), injectedTabIds)
|
|
313
|
+
setActiveTab(requested === 'deals' && isDealsAccessReady && !canViewDeals ? 'people' : requested)
|
|
314
|
+
}, [searchParams, injectedTabIds, isDealsAccessReady, canViewDeals])
|
|
328
315
|
|
|
329
316
|
// Tags
|
|
330
317
|
const handleTagsChange = React.useCallback((nextTags: TagSummary[]) => {
|
|
@@ -492,6 +479,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
492
479
|
|
|
493
480
|
{/* Persistent company header */}
|
|
494
481
|
<CompanyDetailHeader
|
|
482
|
+
actions={<InjectionSpot spotId={extensionPoints.hosts.companyHeaderActions.spotId} context={injectionContext} data={data} disabled={isSaving} />}
|
|
495
483
|
data={data}
|
|
496
484
|
onTagsChange={handleTagsChange}
|
|
497
485
|
tagsSectionControllerRef={tagsSectionControllerRef}
|
|
@@ -506,6 +494,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
506
494
|
<CompanyKpiBar data={data} />
|
|
507
495
|
|
|
508
496
|
{/* Two-zone layout: zone1 = form, zone2 = tabs */}
|
|
497
|
+
<CustomerDetailSidebar spotId={extensionPoints.hosts.companySidebar.spotId} context={injectionContext} data={data} onDataChange={setData} disabled={isSaving}>
|
|
509
498
|
<CollapsibleZoneLayout
|
|
510
499
|
pageType="company-v2"
|
|
511
500
|
entityName={companyName}
|
|
@@ -517,6 +506,10 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
517
506
|
embedded
|
|
518
507
|
trackDirtyWhenEmbedded
|
|
519
508
|
injectionSpotId={extensionPoints.hosts.companyForm.spotId}
|
|
509
|
+
entityId="customers.company"
|
|
510
|
+
resourceKind="customers.company"
|
|
511
|
+
resourceId={companyId}
|
|
512
|
+
injectionGroupAliases={{ details: 'identity', profile: 'businessProfile' }}
|
|
520
513
|
entityIds={[E.customers.customer_entity, E.customers.customer_company_profile]}
|
|
521
514
|
schema={formSchema}
|
|
522
515
|
fields={formFields}
|
|
@@ -536,6 +529,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
536
529
|
<CompanyDetailTabs
|
|
537
530
|
activeTab={activeTab}
|
|
538
531
|
onTabChange={handleTabChange}
|
|
532
|
+
isLoadingInjectedTabs={injectedTabsLoading}
|
|
539
533
|
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
540
534
|
peopleCount={data.counts?.people ?? 0}
|
|
541
535
|
dealsCount={dealCount}
|
|
@@ -621,7 +615,10 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
621
615
|
}
|
|
622
616
|
/>
|
|
623
617
|
|
|
618
|
+
</CustomerDetailSidebar>
|
|
619
|
+
|
|
624
620
|
{/* UMES footer injection */}
|
|
621
|
+
|
|
625
622
|
<InjectionSpot spotId={extensionPoints.hosts.companyFooter.spotId} context={injectionContext} data={data} />
|
|
626
623
|
|
|
627
624
|
{/* Schedule Activity Dialog */}
|
|
@@ -1,60 +1,25 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { useCustomerInjectedTabs } from '../../../../../components/detail/useCustomerInjectedTabs'
|
|
3
|
+
import { DEAL_DETAIL_TAB_IDS } from '../../../../../components/detail/DealDetailTabs'
|
|
4
|
+
import { extensionPoints } from '../../../../../extension-points'
|
|
3
5
|
import type { DealDetailPayload } from './types'
|
|
4
6
|
|
|
5
|
-
type InjectedTab = {
|
|
6
|
-
id: string
|
|
7
|
-
label: string
|
|
8
|
-
priority: number
|
|
9
|
-
render: () => React.ReactNode
|
|
10
|
-
}
|
|
11
|
-
|
|
12
7
|
type UseDealInjectedTabsOptions = {
|
|
13
8
|
injectionContext: unknown
|
|
14
9
|
data: DealDetailPayload | null
|
|
15
10
|
setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>
|
|
16
11
|
}
|
|
17
12
|
|
|
18
|
-
type UseDealInjectedTabsResult = {
|
|
19
|
-
injectedTabs: InjectedTab[]
|
|
20
|
-
injectedTabMap: Map<string, () => React.ReactNode>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
13
|
export function useDealInjectedTabs({
|
|
24
14
|
injectionContext,
|
|
25
15
|
data,
|
|
26
16
|
setData,
|
|
27
|
-
}: UseDealInjectedTabsOptions)
|
|
28
|
-
|
|
17
|
+
}: UseDealInjectedTabsOptions) {
|
|
18
|
+
return useCustomerInjectedTabs({
|
|
19
|
+
spotId: extensionPoints.hosts.dealTabs.spotId,
|
|
29
20
|
context: injectionContext,
|
|
30
|
-
|
|
21
|
+
data,
|
|
22
|
+
onDataChange: setData,
|
|
23
|
+
nativeTabIds: DEAL_DETAIL_TAB_IDS,
|
|
31
24
|
})
|
|
32
|
-
|
|
33
|
-
const injectedTabs = React.useMemo<InjectedTab[]>(
|
|
34
|
-
() =>
|
|
35
|
-
(injectedTabWidgets ?? [])
|
|
36
|
-
.filter((widget) => (widget.placement?.kind ?? 'tab') === 'tab')
|
|
37
|
-
.map((widget) => {
|
|
38
|
-
const tabId = widget.placement?.groupId ?? widget.widgetId
|
|
39
|
-
const label = widget.placement?.groupLabel ?? widget.module.metadata.title ?? tabId
|
|
40
|
-
const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
|
|
41
|
-
const render = () => (
|
|
42
|
-
<widget.module.Widget
|
|
43
|
-
context={injectionContext}
|
|
44
|
-
data={data}
|
|
45
|
-
onDataChange={(next: unknown) => setData(next as DealDetailPayload)}
|
|
46
|
-
/>
|
|
47
|
-
)
|
|
48
|
-
return { id: tabId, label, priority, render }
|
|
49
|
-
})
|
|
50
|
-
.sort((left, right) => right.priority - left.priority),
|
|
51
|
-
[data, injectedTabWidgets, injectionContext, setData],
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
const injectedTabMap = React.useMemo(
|
|
55
|
-
() => new Map(injectedTabs.map((tab) => [tab.id, tab.render])),
|
|
56
|
-
[injectedTabs],
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
return { injectedTabs, injectedTabMap }
|
|
60
25
|
}
|
|
@@ -10,6 +10,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
|
10
10
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
11
11
|
import { AttachmentsSection, ErrorMessage, LoadingMessage, NotesSection, RecordNotFoundState } from '@open-mercato/ui/backend/detail'
|
|
12
12
|
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
13
|
+
import { CustomerDetailSidebar } from '../../../../components/detail/CustomerDetailSidebar'
|
|
13
14
|
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
14
15
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
15
16
|
import { CollapsibleZoneLayout, type ZoneSectionDescriptor } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
|
|
@@ -100,7 +101,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
100
101
|
[detailTranslator, runMutationWithContext],
|
|
101
102
|
)
|
|
102
103
|
|
|
103
|
-
const { injectedTabs, injectedTabMap } = useDealInjectedTabs({
|
|
104
|
+
const { injectedTabs, injectedTabMap, loading: injectedTabsLoading } = useDealInjectedTabs({
|
|
104
105
|
injectionContext,
|
|
105
106
|
data,
|
|
106
107
|
setData,
|
|
@@ -428,8 +429,6 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
428
429
|
const zone2Content = (
|
|
429
430
|
<div className="rounded-[10px] border border-border bg-card px-5 py-5">
|
|
430
431
|
{(() => {
|
|
431
|
-
const injected = injectedTabMap.get(activeTab)
|
|
432
|
-
if (injected) return injected()
|
|
433
432
|
|
|
434
433
|
if (activeTab === 'activities') {
|
|
435
434
|
const activityEntitySelection = activityEntities.length > 1 ? (
|
|
@@ -618,6 +617,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
618
617
|
|
|
619
618
|
return null
|
|
620
619
|
})()}
|
|
620
|
+
{injectedTabMap.get(activeTab)?.()}
|
|
621
621
|
</div>
|
|
622
622
|
)
|
|
623
623
|
|
|
@@ -628,6 +628,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
628
628
|
<InjectionSpot spotId={extensionPoints.hosts.dealHeader.spotId} context={injectionContext} data={data} />
|
|
629
629
|
|
|
630
630
|
<DealDetailHeader
|
|
631
|
+
actions={<InjectionSpot spotId={extensionPoints.hosts.dealHeaderActions.spotId} context={{ ...injectionContext, isDirty, isSaving }} data={data} disabled={isSaving} />}
|
|
631
632
|
deal={data.deal}
|
|
632
633
|
owner={data.owner}
|
|
633
634
|
people={data.people}
|
|
@@ -661,7 +662,9 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
661
662
|
)}
|
|
662
663
|
/>
|
|
663
664
|
|
|
665
|
+
<CustomerDetailSidebar spotId={extensionPoints.hosts.dealSidebar.spotId} context={injectionContext} data={data} onDataChange={setData} disabled={isSaving}>
|
|
664
666
|
<DealDetailTabs
|
|
667
|
+
isLoadingInjectedTabs={injectedTabsLoading}
|
|
665
668
|
activeTab={activeTab}
|
|
666
669
|
onTabChange={handleTabChange}
|
|
667
670
|
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
@@ -678,6 +681,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
678
681
|
zone2={zone2Content}
|
|
679
682
|
/>
|
|
680
683
|
</DealDetailTabs>
|
|
684
|
+
</CustomerDetailSidebar>
|
|
681
685
|
|
|
682
686
|
<InjectionSpot spotId={extensionPoints.hosts.dealFooter.spotId} context={injectionContext} data={data} />
|
|
683
687
|
</div>
|
package/src/modules/customers/backend/customers/deals/pipeline/components/QuickDealDialog.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { E } from '#generated/entities.ids.generated'
|
|
5
6
|
import {
|
|
6
7
|
Dialog,
|
|
7
8
|
DialogContent,
|
|
@@ -14,6 +15,7 @@ import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/b
|
|
|
14
15
|
import { ComboboxInput } from '@open-mercato/ui/backend/inputs/ComboboxInput'
|
|
15
16
|
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
16
17
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
18
|
+
import { extensionPoints } from '../../../../../extension-points'
|
|
17
19
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
18
20
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
19
21
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -124,14 +126,8 @@ export function QuickDealDialog({
|
|
|
124
126
|
if (open) setFormInstanceKey((current) => current + 1)
|
|
125
127
|
}, [open])
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
resourceKind: string
|
|
130
|
-
retryLastMutation: () => Promise<boolean>
|
|
131
|
-
}>({
|
|
132
|
-
contextId: QUICK_DEAL_CONTEXT_ID,
|
|
133
|
-
blockedMessage: translateWithFallback(t, 'ui.forms.flash.saveBlocked', 'Save blocked by validation'),
|
|
134
|
-
})
|
|
129
|
+
|
|
130
|
+
const { runMutation, retryLastMutation } = useGuardedMutation({ contextId: QUICK_DEAL_CONTEXT_ID, blockedMessage: translateWithFallback(t, 'ui.forms.flash.saveBlocked', 'Save blocked by validation') })
|
|
135
131
|
|
|
136
132
|
const titleRequiredMessage = translateWithFallback(
|
|
137
133
|
t,
|
|
@@ -355,33 +351,28 @@ export function QuickDealDialog({
|
|
|
355
351
|
if (values.description && values.description.trim().length) {
|
|
356
352
|
payload.description = values.description
|
|
357
353
|
}
|
|
358
|
-
const
|
|
359
|
-
createCrud('customers/deals', payload, {
|
|
354
|
+
const { result } = await runMutation({
|
|
355
|
+
operation: () => createCrud<{ id?: string; entityId?: string }>('customers/deals', payload, {
|
|
360
356
|
errorMessage: translateWithFallback(t, 'customers.deals.create.error', 'Failed to create deal.'),
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
context: {
|
|
365
|
-
formId: QUICK_DEAL_CONTEXT_ID,
|
|
366
|
-
resourceKind: 'customers.deal',
|
|
367
|
-
retryLastMutation,
|
|
368
|
-
},
|
|
357
|
+
}),
|
|
358
|
+
context: { formId: QUICK_DEAL_CONTEXT_ID, resourceKind: 'customers.deal', retryLastMutation },
|
|
359
|
+
mutationPayload: payload,
|
|
369
360
|
})
|
|
370
361
|
flash(
|
|
371
|
-
translateWithFallback(
|
|
372
|
-
t,
|
|
373
|
-
'customers.deals.kanban.quickDeal.success',
|
|
374
|
-
'Deal added to {stage}.',
|
|
375
|
-
{ stage: context.pipelineStageLabel },
|
|
376
|
-
),
|
|
362
|
+
translateWithFallback(t, 'customers.deals.kanban.quickDeal.success', 'Deal added to {stage}.', { stage: context.pipelineStageLabel }),
|
|
377
363
|
'success',
|
|
378
364
|
)
|
|
379
|
-
|
|
380
|
-
onClose()
|
|
365
|
+
return { resourceId: result?.id ?? result?.entityId }
|
|
381
366
|
},
|
|
382
|
-
[context, currentUserId,
|
|
367
|
+
[context, currentUserId, t, titleRequiredMessage, runMutation, retryLastMutation],
|
|
383
368
|
)
|
|
384
369
|
|
|
370
|
+
const handleSubmitSuccess = React.useCallback(() => {
|
|
371
|
+
if (!context) return
|
|
372
|
+
onCreated()
|
|
373
|
+
onClose()
|
|
374
|
+
}, [context, onCreated, onClose])
|
|
375
|
+
|
|
385
376
|
if (!context) return null
|
|
386
377
|
|
|
387
378
|
return (
|
|
@@ -409,6 +400,12 @@ export function QuickDealDialog({
|
|
|
409
400
|
<CrudForm<QuickDealFormValues>
|
|
410
401
|
key={`${context.pipelineStageId}:${formInstanceKey}`}
|
|
411
402
|
embedded
|
|
403
|
+
formId={QUICK_DEAL_CONTEXT_ID}
|
|
404
|
+
entityId="customers.deal"
|
|
405
|
+
entityIds={[E.customers.customer_deal]}
|
|
406
|
+
resourceKind="customers.deal"
|
|
407
|
+
injectionSpotId={extensionPoints.hosts.dealForm.spotId}
|
|
408
|
+
injectionGroupAliases={{ details: 'basic' }}
|
|
412
409
|
fields={fields}
|
|
413
410
|
groups={groups}
|
|
414
411
|
// The "More details" group starts collapsed; users can expand to set
|
|
@@ -419,6 +416,7 @@ export function QuickDealDialog({
|
|
|
419
416
|
schema={formSchema}
|
|
420
417
|
submitLabel={translateWithFallback(t, 'customers.deals.kanban.quickDeal.submit', 'Add deal')}
|
|
421
418
|
onSubmit={handleSubmit}
|
|
419
|
+
onSubmitSuccess={handleSubmitSuccess}
|
|
422
420
|
/>
|
|
423
421
|
</DialogContent>
|
|
424
422
|
</Dialog>
|
|
@@ -10,6 +10,7 @@ import { E } from '#generated/entities.ids.generated'
|
|
|
10
10
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
11
11
|
import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
12
12
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
13
|
+
import { extensionPoints } from '../../../../extension-points'
|
|
13
14
|
import {
|
|
14
15
|
buildPersonPayload,
|
|
15
16
|
createPersonFormFields,
|
|
@@ -52,6 +53,10 @@ export default function CreatePersonPage() {
|
|
|
52
53
|
fields={fields}
|
|
53
54
|
groups={groups}
|
|
54
55
|
initialValues={initialValues}
|
|
56
|
+
injectionSpotId={extensionPoints.hosts.personForm.spotId}
|
|
57
|
+
legacyInjectionSpotId="crud-form:customers.customer_entity"
|
|
58
|
+
entityId="customers.person"
|
|
59
|
+
resourceKind="customers.person"
|
|
55
60
|
entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
|
|
56
61
|
submitLabel={t('customers.people.form.submit')}
|
|
57
62
|
cancelHref={returnTo ?? '/backend/customers/people'}
|
|
@@ -127,6 +132,10 @@ export default function CreatePersonPage() {
|
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
flash(t('customers.people.form.success'), 'success')
|
|
135
|
+
return newId ? { resourceId: newId } : undefined
|
|
136
|
+
}}
|
|
137
|
+
onSubmitSuccess={async (_values, result) => {
|
|
138
|
+
const newId = result?.resourceId
|
|
130
139
|
if (returnTo) router.push(returnTo)
|
|
131
140
|
else if (newId) router.push(`/backend/customers/people-v2/${newId}`)
|
|
132
141
|
else router.push('/backend/customers/people')
|