@open-mercato/core 0.10.1-develop.7761.1.9dfaf470ed → 0.10.1-develop.7762.1.657bb7236b
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/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/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/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/integrations/widgets/injection/external-ids/widget.client.tsx +4 -4
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')
|
|
@@ -23,7 +23,9 @@ import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/us
|
|
|
23
23
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
24
24
|
import { AttachmentsSection, ErrorMessage, LoadingMessage, RecordNotFoundState, type SectionAction } from '@open-mercato/ui/backend/detail'
|
|
25
25
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
26
|
-
import { InjectionSpot
|
|
26
|
+
import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
27
|
+
import { useCustomerInjectedTabs } from '../../../../components/detail/useCustomerInjectedTabs'
|
|
28
|
+
import { CustomerDetailSidebar } from '../../../../components/detail/CustomerDetailSidebar'
|
|
27
29
|
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
28
30
|
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
29
31
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
@@ -38,7 +40,7 @@ import type { TagSummary } from '../../../../components/detail/types'
|
|
|
38
40
|
import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../../components/detail/ScheduleActivityDialog'
|
|
39
41
|
import { PersonDetailHeader } from '../../../../components/detail/PersonDetailHeader'
|
|
40
42
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
41
|
-
import { PersonDetailTabs, resolveLegacyTab, type PersonTabId } from '../../../../components/detail/PersonDetailTabs'
|
|
43
|
+
import { PersonDetailTabs, PERSON_DETAIL_TAB_IDS, resolveLegacyTab, type PersonTabId } from '../../../../components/detail/PersonDetailTabs'
|
|
42
44
|
import { AddressesSection } from '../../../../components/detail/AddressesSection'
|
|
43
45
|
import { PersonCompaniesSection } from '../../../../components/detail/PersonCompaniesSection'
|
|
44
46
|
import { MobilePersonDetail } from '../../../../components/detail/MobilePersonDetail'
|
|
@@ -207,9 +209,11 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
207
209
|
resourceKind: 'customers.person',
|
|
208
210
|
resourceId: currentPersonId ?? (id ?? undefined),
|
|
209
211
|
data,
|
|
212
|
+
isDirty,
|
|
213
|
+
isSaving,
|
|
210
214
|
retryLastMutation,
|
|
211
215
|
}),
|
|
212
|
-
[currentPersonId, data, id, mutationContextId, retryLastMutation],
|
|
216
|
+
[currentPersonId, data, isDirty, isSaving, id, mutationContextId, retryLastMutation],
|
|
213
217
|
)
|
|
214
218
|
const runMutationWithContext = React.useCallback(
|
|
215
219
|
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
@@ -298,37 +302,14 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
298
302
|
setScheduleDialogOpen(true)
|
|
299
303
|
}, [])
|
|
300
304
|
|
|
301
|
-
|
|
302
|
-
|
|
305
|
+
const { injectedTabs, injectedTabMap, loading: injectedTabsLoading } = useCustomerInjectedTabs({
|
|
306
|
+
spotId: extensionPoints.hosts.personTabs.spotId,
|
|
303
307
|
context: injectionContext,
|
|
304
|
-
|
|
308
|
+
data,
|
|
309
|
+
onDataChange: setData,
|
|
310
|
+
nativeTabIds: PERSON_DETAIL_TAB_IDS,
|
|
305
311
|
})
|
|
306
312
|
|
|
307
|
-
const injectedTabs = React.useMemo(
|
|
308
|
-
() =>
|
|
309
|
-
(injectedTabWidgets ?? [])
|
|
310
|
-
.filter((widget) => (widget.placement?.kind ?? 'tab') === 'tab')
|
|
311
|
-
.map((widget) => {
|
|
312
|
-
const tabId = widget.placement?.groupId ?? widget.widgetId
|
|
313
|
-
const label = widget.placement?.groupLabel
|
|
314
|
-
? t(widget.placement.groupLabel, widget.placement.groupLabel)
|
|
315
|
-
: widget.module.metadata.title ?? tabId
|
|
316
|
-
const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
|
|
317
|
-
const render = () => (
|
|
318
|
-
<widget.module.Widget
|
|
319
|
-
context={injectionContext}
|
|
320
|
-
data={data}
|
|
321
|
-
onDataChange={(next: unknown) => setData(next as PersonOverview)}
|
|
322
|
-
/>
|
|
323
|
-
)
|
|
324
|
-
return { id: tabId, label, priority, render }
|
|
325
|
-
})
|
|
326
|
-
.sort((a, b) => b.priority - a.priority),
|
|
327
|
-
[data, injectedTabWidgets, injectionContext, t],
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
const injectedTabMap = React.useMemo(() => new Map(injectedTabs.map((tab) => [tab.id, tab.render])), [injectedTabs])
|
|
331
|
-
|
|
332
313
|
const injectedTabIds = React.useMemo(() => injectedTabs.map((tab) => tab.id), [injectedTabs])
|
|
333
314
|
const initialTab = React.useMemo(
|
|
334
315
|
() => resolveLegacyTab(searchParams?.get('tab'), injectedTabIds),
|
|
@@ -530,6 +511,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
530
511
|
|
|
531
512
|
{/* Persistent person header */}
|
|
532
513
|
<PersonDetailHeader
|
|
514
|
+
actions={<InjectionSpot spotId={extensionPoints.hosts.personHeaderActions.spotId} context={injectionContext} data={data} disabled={isSaving} />}
|
|
533
515
|
data={data}
|
|
534
516
|
onTagsChange={handleTagsChange}
|
|
535
517
|
tagsSectionControllerRef={tagsSectionControllerRef}
|
|
@@ -554,6 +536,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
554
536
|
/>
|
|
555
537
|
|
|
556
538
|
{/* Zone content shared between desktop (CollapsibleZoneLayout) and mobile (MobilePersonDetail). */}
|
|
539
|
+
<CustomerDetailSidebar spotId={extensionPoints.hosts.personSidebar.spotId} context={injectionContext} data={data} onDataChange={setData} disabled={isSaving}>
|
|
557
540
|
{(() => {
|
|
558
541
|
const zone1Content = (
|
|
559
542
|
<div ref={formWrapperRef}>
|
|
@@ -561,6 +544,10 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
561
544
|
embedded
|
|
562
545
|
trackDirtyWhenEmbedded
|
|
563
546
|
injectionSpotId={extensionPoints.hosts.personForm.spotId}
|
|
547
|
+
entityId="customers.person"
|
|
548
|
+
resourceKind="customers.person"
|
|
549
|
+
resourceId={personId}
|
|
550
|
+
injectionGroupAliases={{ details: 'personalData' }}
|
|
564
551
|
entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
|
|
565
552
|
schema={formSchema}
|
|
566
553
|
fields={fields}
|
|
@@ -580,6 +567,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
580
567
|
<PersonDetailTabs
|
|
581
568
|
activeTab={activeTab}
|
|
582
569
|
onTabChange={handleTabChange}
|
|
570
|
+
isLoadingInjectedTabs={injectedTabsLoading}
|
|
583
571
|
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
584
572
|
activitiesCount={interactionCount}
|
|
585
573
|
dealsCount={dealCount}
|
|
@@ -591,8 +579,6 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
591
579
|
<div className="min-w-0">
|
|
592
580
|
{(() => {
|
|
593
581
|
// Injected tab content
|
|
594
|
-
const injected = injectedTabMap.get(activeTab)
|
|
595
|
-
if (injected) return injected()
|
|
596
582
|
|
|
597
583
|
if (activeTab === 'activities') {
|
|
598
584
|
return (
|
|
@@ -719,6 +705,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
719
705
|
|
|
720
706
|
return null
|
|
721
707
|
})()}
|
|
708
|
+
{injectedTabMap.get(activeTab)?.()}
|
|
722
709
|
</div>
|
|
723
710
|
</PersonDetailTabs>
|
|
724
711
|
)
|
|
@@ -740,6 +727,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
740
727
|
/>
|
|
741
728
|
)
|
|
742
729
|
})()}
|
|
730
|
+
</CustomerDetailSidebar>
|
|
743
731
|
|
|
744
732
|
{/* UMES footer injection */}
|
|
745
733
|
<InjectionSpot spotId={extensionPoints.hosts.personFooter.spotId} context={injectionContext} data={data} />
|
|
@@ -29,6 +29,7 @@ type CompanyDetailHeaderProps = {
|
|
|
29
29
|
onDelete: () => Promise<void>
|
|
30
30
|
isDirty: boolean
|
|
31
31
|
isSaving: boolean
|
|
32
|
+
actions?: React.ReactNode
|
|
32
33
|
onFocusField?: (fieldName: string) => void
|
|
33
34
|
onDataReload?: () => void
|
|
34
35
|
}
|
|
@@ -57,6 +58,7 @@ export function CompanyDetailHeader({
|
|
|
57
58
|
onDelete,
|
|
58
59
|
isDirty,
|
|
59
60
|
isSaving,
|
|
61
|
+
actions,
|
|
60
62
|
onFocusField,
|
|
61
63
|
onDataReload,
|
|
62
64
|
}: CompanyDetailHeaderProps) {
|
|
@@ -199,6 +201,7 @@ export function CompanyDetailHeader({
|
|
|
199
201
|
{/* Right side: actions */}
|
|
200
202
|
<div className="flex w-full shrink-0 flex-col items-start gap-3 sm:w-auto sm:items-end">
|
|
201
203
|
<div className="flex w-full flex-wrap items-center justify-start gap-2 sm:w-auto sm:justify-end">
|
|
204
|
+
{actions}
|
|
202
205
|
<SendObjectMessageDialog
|
|
203
206
|
object={{
|
|
204
207
|
entityModule: 'customers',
|
|
@@ -17,6 +17,8 @@ import { registerComponent } from '@open-mercato/shared/modules/widgets/componen
|
|
|
17
17
|
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
18
18
|
import { useDealsAccess } from './useDealsAccess'
|
|
19
19
|
|
|
20
|
+
export const COMPANY_DETAIL_TAB_IDS = ['people', 'deals', 'activity-log', 'changelog', 'files'] as const
|
|
21
|
+
|
|
20
22
|
export type CompanyTabId =
|
|
21
23
|
| 'people'
|
|
22
24
|
| 'deals'
|
|
@@ -38,6 +40,7 @@ export type CompanyDetailTabsProps = {
|
|
|
38
40
|
activeTab: CompanyTabId
|
|
39
41
|
onTabChange: (tab: CompanyTabId) => void
|
|
40
42
|
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
43
|
+
isLoadingInjectedTabs?: boolean
|
|
41
44
|
hiddenTabIds?: string[]
|
|
42
45
|
peopleCount?: number
|
|
43
46
|
dealsCount?: number
|
|
@@ -57,10 +60,11 @@ const LEGACY_TAB_MAP: Record<string, CompanyTabId> = {
|
|
|
57
60
|
analysis: 'people',
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
export function resolveLegacyTab(tab: string | null | undefined): CompanyTabId {
|
|
63
|
+
export function resolveLegacyTab(tab: string | null | undefined, knownTabIds?: Iterable<string>): CompanyTabId {
|
|
61
64
|
if (!tab) return 'people'
|
|
62
65
|
if (LEGACY_TAB_MAP[tab]) return LEGACY_TAB_MAP[tab]
|
|
63
|
-
|
|
66
|
+
if ((COMPANY_DETAIL_TAB_IDS as readonly string[]).includes(tab) || (knownTabIds && new Set(knownTabIds).has(tab))) return tab
|
|
67
|
+
return 'people'
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
function formatTabCount(count: number): string | number | undefined {
|
|
@@ -72,6 +76,7 @@ function DefaultCompanyDetailTabs({
|
|
|
72
76
|
activeTab,
|
|
73
77
|
onTabChange,
|
|
74
78
|
injectedTabs = [],
|
|
79
|
+
isLoadingInjectedTabs = false,
|
|
75
80
|
hiddenTabIds = [],
|
|
76
81
|
peopleCount = 0,
|
|
77
82
|
dealsCount = 0,
|
|
@@ -81,7 +86,7 @@ function DefaultCompanyDetailTabs({
|
|
|
81
86
|
children,
|
|
82
87
|
}: CompanyDetailTabsProps) {
|
|
83
88
|
const t = useT()
|
|
84
|
-
const { canViewDeals } = useDealsAccess()
|
|
89
|
+
const { canViewDeals, isReady: isDealsAccessReady } = useDealsAccess()
|
|
85
90
|
|
|
86
91
|
const builtInTabs: TabDef[] = React.useMemo(
|
|
87
92
|
() => [
|
|
@@ -127,13 +132,19 @@ function DefaultCompanyDetailTabs({
|
|
|
127
132
|
const hidden = new Set(hiddenTabIds)
|
|
128
133
|
return [
|
|
129
134
|
...builtInTabs,
|
|
130
|
-
...injectedTabs.map((tab) => ({
|
|
135
|
+
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
131
136
|
id: tab.id as CompanyTabId,
|
|
132
137
|
label: tab.label,
|
|
133
138
|
})),
|
|
134
139
|
].filter((tab) => !hidden.has(tab.id))
|
|
135
140
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
136
141
|
|
|
142
|
+
React.useEffect(() => {
|
|
143
|
+
if (isLoadingInjectedTabs || !isDealsAccessReady || allTabs.some((tab) => tab.id === activeTab)) return
|
|
144
|
+
const fallback = allTabs[0]?.id
|
|
145
|
+
if (fallback) onTabChange(fallback)
|
|
146
|
+
}, [activeTab, allTabs, isLoadingInjectedTabs, onTabChange, isDealsAccessReady])
|
|
147
|
+
|
|
137
148
|
return (
|
|
138
149
|
<div>
|
|
139
150
|
{/* Tab navigation */}
|
|
@@ -4,7 +4,7 @@ import * as React from 'react'
|
|
|
4
4
|
import { Building2 } from 'lucide-react'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
6
|
import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
7
|
-
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
7
|
+
import { CrudForm, type CrudField, type CrudFormSubmitResult } from '@open-mercato/ui/backend/CrudForm'
|
|
8
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
9
|
import { createCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
10
10
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
@@ -12,6 +12,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
12
12
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
13
13
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
14
14
|
import { E } from '#generated/entities.ids.generated'
|
|
15
|
+
import { extensionPoints } from '../../extension-points'
|
|
15
16
|
import {
|
|
16
17
|
buildPersonPayload,
|
|
17
18
|
createPersonFormFields,
|
|
@@ -134,22 +135,27 @@ export function CreatePersonDialog({
|
|
|
134
135
|
? await runGuardedMutation(operation, payload)
|
|
135
136
|
: await operation()
|
|
136
137
|
|
|
138
|
+
const newId = response?.result?.id ?? response?.result?.entityId ?? ''
|
|
137
139
|
flash(
|
|
138
140
|
companyId
|
|
139
141
|
? t('customers.people.createDialog.success', 'Person created and linked to company')
|
|
140
142
|
: t('customers.people.createDialog.successPlain', 'Person created'),
|
|
141
143
|
'success',
|
|
142
144
|
)
|
|
143
|
-
|
|
145
|
+
return newId ? { resourceId: newId } : undefined
|
|
146
|
+
}, [companyId, organizationId, runGuardedMutation, t])
|
|
147
|
+
|
|
148
|
+
const handleSubmitSuccess = React.useCallback(async (values: PersonFormValues, result: CrudFormSubmitResult | void) => {
|
|
149
|
+
const payload = buildPersonPayload(values, organizationId)
|
|
144
150
|
const displayNameFromPayload = typeof payload.displayName === 'string' ? payload.displayName : ''
|
|
145
151
|
onPersonCreated?.({
|
|
146
|
-
id:
|
|
152
|
+
id: result?.resourceId ?? '',
|
|
147
153
|
displayName: displayNameFromPayload,
|
|
148
154
|
companyId: companyId ?? null,
|
|
149
155
|
companyName: companyName ?? null,
|
|
150
156
|
})
|
|
151
157
|
onClose()
|
|
152
|
-
}, [companyId, companyName, onClose, onPersonCreated, organizationId
|
|
158
|
+
}, [companyId, companyName, onClose, onPersonCreated, organizationId])
|
|
153
159
|
|
|
154
160
|
const handleKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
155
161
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
@@ -179,6 +185,10 @@ export function CreatePersonDialog({
|
|
|
179
185
|
<CrudForm<PersonFormValues>
|
|
180
186
|
key={`${companyId ?? 'no-company'}:${formInstanceKey}`}
|
|
181
187
|
embedded
|
|
188
|
+
injectionSpotId={extensionPoints.hosts.personForm.spotId}
|
|
189
|
+
legacyInjectionSpotId="crud-form:customers.customer_entity"
|
|
190
|
+
entityId="customers.person"
|
|
191
|
+
resourceKind="customers.person"
|
|
182
192
|
entityIds={[E.customers.customer_entity, E.customers.customer_person_profile]}
|
|
183
193
|
fields={fields}
|
|
184
194
|
groups={groups}
|
|
@@ -186,6 +196,7 @@ export function CreatePersonDialog({
|
|
|
186
196
|
submitLabel={t('customers.people.createDialog.submit', 'Create person')}
|
|
187
197
|
schema={formSchema}
|
|
188
198
|
onSubmit={handleSubmit}
|
|
199
|
+
onSubmitSuccess={handleSubmitSuccess}
|
|
189
200
|
extraActions={(
|
|
190
201
|
<Button type="button" variant="outline" onClick={onClose}>
|
|
191
202
|
{t('customers.people.createDialog.cancel', 'Cancel')}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { InjectionSpot, useInjectionWidgets } from '@open-mercato/ui/backend/injection/InjectionSpot'
|
|
5
|
+
|
|
6
|
+
export function CustomerDetailSidebar<TData>({ spotId, context, data, onDataChange, disabled, children }: {
|
|
7
|
+
spotId: string
|
|
8
|
+
context: unknown
|
|
9
|
+
data: TData
|
|
10
|
+
onDataChange?: (data: TData) => void
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
}) {
|
|
14
|
+
const { widgets } = useInjectionWidgets(spotId, { context, triggerOnLoad: true })
|
|
15
|
+
const sidebarRef = React.useRef<HTMLElement>(null)
|
|
16
|
+
const [hasContent, setHasContent] = React.useState(false)
|
|
17
|
+
React.useLayoutEffect(() => {
|
|
18
|
+
const sidebar = sidebarRef.current
|
|
19
|
+
if (!sidebar) {
|
|
20
|
+
setHasContent(false)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
const update = () => setHasContent(Array.from(sidebar.childNodes).some((node) => (
|
|
24
|
+
node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && Boolean(node.textContent?.trim()))
|
|
25
|
+
)))
|
|
26
|
+
update()
|
|
27
|
+
const observer = new MutationObserver(update)
|
|
28
|
+
observer.observe(sidebar, { childList: true, subtree: true, characterData: true })
|
|
29
|
+
return () => observer.disconnect()
|
|
30
|
+
}, [widgets, data])
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex min-w-0 flex-col gap-4 lg:flex-row" data-customer-detail-layout>
|
|
33
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
34
|
+
{widgets.length ? (
|
|
35
|
+
<aside ref={sidebarRef} className={hasContent ? 'w-full space-y-4 lg:w-80 lg:shrink-0' : 'hidden'} data-customer-detail-sidebar={spotId}>
|
|
36
|
+
<InjectionSpot spotId={spotId} context={context} data={data} onDataChange={onDataChange} disabled={disabled} widgetsOverride={widgets} />
|
|
37
|
+
</aside>
|
|
38
|
+
) : null}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -45,6 +45,7 @@ type DealDetailHeaderProps = {
|
|
|
45
45
|
onDelete: () => Promise<void> | void
|
|
46
46
|
isDirty: boolean
|
|
47
47
|
isSaving: boolean
|
|
48
|
+
actions?: React.ReactNode
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function formatCurrency(amount: string | null, currency: string | null): string | null {
|
|
@@ -196,6 +197,7 @@ export function DealDetailHeader({
|
|
|
196
197
|
onDelete,
|
|
197
198
|
isDirty,
|
|
198
199
|
isSaving,
|
|
200
|
+
actions,
|
|
199
201
|
}: DealDetailHeaderProps) {
|
|
200
202
|
const t = useT()
|
|
201
203
|
const amountLabel = React.useMemo(
|
|
@@ -304,7 +306,8 @@ export function DealDetailHeader({
|
|
|
304
306
|
</div>
|
|
305
307
|
</div>
|
|
306
308
|
|
|
307
|
-
<div className="flex items-center gap-2 xl:justify-end">
|
|
309
|
+
<div className="flex flex-wrap items-center gap-2 xl:justify-end">
|
|
310
|
+
{actions}
|
|
308
311
|
{canMoveStage ? (
|
|
309
312
|
<StageActionButton
|
|
310
313
|
stageOptions={stageOptions}
|
|
@@ -7,6 +7,8 @@ import { registerComponent } from '@open-mercato/shared/modules/widgets/componen
|
|
|
7
7
|
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
8
8
|
import { Tabs, TabsList, TabsTrigger } from '@open-mercato/ui/primitives/tabs'
|
|
9
9
|
|
|
10
|
+
export const DEAL_DETAIL_TAB_IDS = ['activities', 'people', 'companies', 'notes', 'files', 'changelog'] as const
|
|
11
|
+
|
|
10
12
|
export type DealTabId =
|
|
11
13
|
| 'activities'
|
|
12
14
|
| 'people'
|
|
@@ -29,6 +31,7 @@ export type DealDetailTabsProps = {
|
|
|
29
31
|
activeTab: DealTabId
|
|
30
32
|
onTabChange: (tab: DealTabId) => void
|
|
31
33
|
injectedTabs?: Array<{ id: string; label: string }>
|
|
34
|
+
isLoadingInjectedTabs?: boolean
|
|
32
35
|
hiddenTabIds?: string[]
|
|
33
36
|
peopleCount?: number
|
|
34
37
|
companiesCount?: number
|
|
@@ -53,6 +56,7 @@ function DefaultDealDetailTabs({
|
|
|
53
56
|
activeTab,
|
|
54
57
|
onTabChange,
|
|
55
58
|
injectedTabs = [],
|
|
59
|
+
isLoadingInjectedTabs = false,
|
|
56
60
|
hiddenTabIds = [],
|
|
57
61
|
peopleCount = 0,
|
|
58
62
|
companiesCount = 0,
|
|
@@ -103,13 +107,19 @@ function DefaultDealDetailTabs({
|
|
|
103
107
|
const hidden = new Set(hiddenTabIds)
|
|
104
108
|
return [
|
|
105
109
|
...builtInTabs,
|
|
106
|
-
...injectedTabs.map((tab) => ({
|
|
110
|
+
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
107
111
|
id: tab.id as DealTabId,
|
|
108
112
|
label: tab.label,
|
|
109
113
|
})),
|
|
110
114
|
].filter((tab) => !hidden.has(tab.id))
|
|
111
115
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
112
116
|
|
|
117
|
+
React.useEffect(() => {
|
|
118
|
+
if (isLoadingInjectedTabs || allTabs.some((tab) => tab.id === activeTab)) return
|
|
119
|
+
const fallback = allTabs[0]?.id
|
|
120
|
+
if (fallback) onTabChange(fallback)
|
|
121
|
+
}, [activeTab, allTabs, isLoadingInjectedTabs, onTabChange])
|
|
122
|
+
|
|
113
123
|
return (
|
|
114
124
|
<div>
|
|
115
125
|
<Tabs
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import { extractResponseEnricherNamespaces } from '@open-mercato/shared/lib/crud/response-enricher-namespaces'
|
|
3
4
|
import * as React from 'react'
|
|
4
5
|
import { z } from 'zod'
|
|
5
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -1226,6 +1227,7 @@ export function DealForm({
|
|
|
1226
1227
|
}
|
|
1227
1228
|
|
|
1228
1229
|
return {
|
|
1230
|
+
...extractResponseEnricherNamespaces(initialValues ?? {}),
|
|
1229
1231
|
id: typeof initialValues?.id === 'string' ? initialValues.id : undefined,
|
|
1230
1232
|
title: initialValues?.title ?? '',
|
|
1231
1233
|
status: initialValues?.status ?? '',
|
|
@@ -1326,6 +1328,9 @@ export function DealForm({
|
|
|
1326
1328
|
schema={schema}
|
|
1327
1329
|
fields={baseFields}
|
|
1328
1330
|
groups={groups}
|
|
1331
|
+
entityId="customers.deal"
|
|
1332
|
+
resourceKind="customers.deal"
|
|
1333
|
+
resourceId={typeof initialValues?.id === 'string' ? initialValues.id : undefined}
|
|
1329
1334
|
entityIds={DEAL_ENTITY_IDS}
|
|
1330
1335
|
initialValues={embeddedInitialValues}
|
|
1331
1336
|
onSubmit={handleSubmit}
|
|
@@ -31,6 +31,7 @@ type PersonDetailHeaderProps = {
|
|
|
31
31
|
onDelete: () => Promise<void>
|
|
32
32
|
isDirty: boolean
|
|
33
33
|
isSaving: boolean
|
|
34
|
+
actions?: React.ReactNode
|
|
34
35
|
/** Callback to focus a specific field in the Zone 1 CrudForm by field name. */
|
|
35
36
|
onFocusField?: (fieldName: string) => void
|
|
36
37
|
/**
|
|
@@ -86,6 +87,7 @@ export function PersonDetailHeader({
|
|
|
86
87
|
onDelete,
|
|
87
88
|
isDirty,
|
|
88
89
|
isSaving,
|
|
90
|
+
actions,
|
|
89
91
|
onFocusField,
|
|
90
92
|
onOpenCompaniesTab,
|
|
91
93
|
onDataReload,
|
|
@@ -246,6 +248,7 @@ export function PersonDetailHeader({
|
|
|
246
248
|
|
|
247
249
|
{/* Right side: actions */}
|
|
248
250
|
<div className="flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end">
|
|
251
|
+
{actions}
|
|
249
252
|
<SendObjectMessageDialog
|
|
250
253
|
object={{
|
|
251
254
|
entityModule: 'customers',
|
|
@@ -19,6 +19,8 @@ import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
|
19
19
|
import { registerComponent } from '@open-mercato/shared/modules/widgets/component-registry'
|
|
20
20
|
import { useRegisteredComponent } from '@open-mercato/ui/backend/injection/useRegisteredComponent'
|
|
21
21
|
|
|
22
|
+
export const PERSON_DETAIL_TAB_IDS = ['activities', 'emails', 'deals', 'companies', 'addresses', 'tasks', 'changelog', 'files'] as const
|
|
23
|
+
|
|
22
24
|
export type PersonTabId =
|
|
23
25
|
| 'activities'
|
|
24
26
|
| 'emails'
|
|
@@ -43,6 +45,7 @@ export type PersonDetailTabsProps = {
|
|
|
43
45
|
activeTab: PersonTabId
|
|
44
46
|
onTabChange: (tab: PersonTabId) => void
|
|
45
47
|
injectedTabs?: Array<{ id: string; label: string }>
|
|
48
|
+
isLoadingInjectedTabs?: boolean
|
|
46
49
|
hiddenTabIds?: string[]
|
|
47
50
|
activitiesCount?: number
|
|
48
51
|
dealsCount?: number
|
|
@@ -72,6 +75,7 @@ function DefaultPersonDetailTabs({
|
|
|
72
75
|
activeTab,
|
|
73
76
|
onTabChange,
|
|
74
77
|
injectedTabs = [],
|
|
78
|
+
isLoadingInjectedTabs = false,
|
|
75
79
|
hiddenTabIds = [],
|
|
76
80
|
activitiesCount = 0,
|
|
77
81
|
dealsCount = 0,
|
|
@@ -141,13 +145,19 @@ function DefaultPersonDetailTabs({
|
|
|
141
145
|
const hidden = new Set(hiddenTabIds)
|
|
142
146
|
return [
|
|
143
147
|
...builtInTabs,
|
|
144
|
-
...injectedTabs.map((tab) => ({
|
|
148
|
+
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
145
149
|
id: tab.id as PersonTabId,
|
|
146
150
|
label: tab.label,
|
|
147
151
|
})),
|
|
148
152
|
].filter((tab) => !hidden.has(tab.id))
|
|
149
153
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
150
154
|
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (isLoadingInjectedTabs || allTabs.some((tab) => tab.id === activeTab)) return
|
|
157
|
+
const fallback = allTabs[0]?.id
|
|
158
|
+
if (fallback) onTabChange(fallback)
|
|
159
|
+
}, [activeTab, allTabs, isLoadingInjectedTabs, onTabChange])
|
|
160
|
+
|
|
151
161
|
return (
|
|
152
162
|
<div>
|
|
153
163
|
{/* Tab navigation — full width above both zones */}
|