@open-mercato/core 0.5.1-develop.2912.8d7b1fef24 → 0.5.1-develop.2924.d13908516e
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/dist/modules/customers/api/companies/[id]/people/route.js +12 -7
- package/dist/modules/customers/api/companies/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +2 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +93 -19
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/commands/people.js +9 -1
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/dist/modules/customers/commands/personCompanyLinks.js +2 -2
- package/dist/modules/customers/commands/personCompanyLinks.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyCard.js +32 -3
- package/dist/modules/customers/components/detail/CompanyCard.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +37 -19
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +7 -4
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js +63 -2
- package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +37 -19
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/TasksSection.js +1 -11
- package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +50 -39
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/events.js +3 -3
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +13 -1
- package/dist/modules/customers/lib/displayName.js.map +2 -2
- package/dist/modules/customers/lib/personCompanies.js +12 -7
- package/dist/modules/customers/lib/personCompanies.js.map +2 -2
- package/dist/modules/customers/lib/personCompanyLinkTable.js +5 -0
- package/dist/modules/customers/lib/personCompanyLinkTable.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +21 -17
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/package.json +3 -3
- package/src/modules/customers/api/companies/[id]/people/route.ts +12 -7
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +2 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +12 -2
- package/src/modules/customers/commands/companies.ts +107 -19
- package/src/modules/customers/commands/people.ts +16 -1
- package/src/modules/customers/commands/personCompanyLinks.ts +3 -2
- package/src/modules/customers/components/detail/CompanyCard.tsx +28 -4
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +18 -2
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +8 -4
- package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +66 -0
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +18 -2
- package/src/modules/customers/components/detail/TasksSection.tsx +1 -8
- package/src/modules/customers/components/formConfig.tsx +59 -40
- package/src/modules/customers/events.ts +3 -3
- package/src/modules/customers/i18n/de.json +10 -0
- package/src/modules/customers/i18n/en.json +10 -0
- package/src/modules/customers/i18n/es.json +10 -0
- package/src/modules/customers/i18n/pl.json +10 -0
- package/src/modules/customers/lib/displayName.ts +19 -0
- package/src/modules/customers/lib/personCompanies.ts +12 -7
- package/src/modules/customers/lib/personCompanyLinkTable.ts +14 -0
- package/src/modules/workflows/lib/activity-executor.ts +21 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.5.1-develop.
|
|
3
|
+
"version": "0.5.1-develop.2924.d13908516e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -237,10 +237,10 @@
|
|
|
237
237
|
"ts-pattern": "^5.0.0"
|
|
238
238
|
},
|
|
239
239
|
"peerDependencies": {
|
|
240
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
240
|
+
"@open-mercato/shared": "0.5.1-develop.2924.d13908516e"
|
|
241
241
|
},
|
|
242
242
|
"devDependencies": {
|
|
243
|
-
"@open-mercato/shared": "0.5.1-develop.
|
|
243
|
+
"@open-mercato/shared": "0.5.1-develop.2924.d13908516e",
|
|
244
244
|
"@testing-library/dom": "^10.4.1",
|
|
245
245
|
"@testing-library/jest-dom": "^6.9.1",
|
|
246
246
|
"@testing-library/react": "^16.3.1",
|
|
@@ -13,7 +13,10 @@ import {
|
|
|
13
13
|
CustomerPersonCompanyLink,
|
|
14
14
|
CustomerPersonProfile,
|
|
15
15
|
} from '../../../../data/entities'
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
filterActivePersonCompanyLinks,
|
|
18
|
+
withActiveCustomerPersonCompanyLinkFilter,
|
|
19
|
+
} from '../../../../lib/personCompanyLinkTable'
|
|
17
20
|
|
|
18
21
|
const paramsSchema = z.object({
|
|
19
22
|
id: z.string().uuid(),
|
|
@@ -129,12 +132,14 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
129
132
|
},
|
|
130
133
|
'customers.companies.people.GET',
|
|
131
134
|
)
|
|
132
|
-
const links =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
const links = filterActivePersonCompanyLinks(
|
|
136
|
+
await findWithDecryption(
|
|
137
|
+
em,
|
|
138
|
+
CustomerPersonCompanyLink,
|
|
139
|
+
linkWhere,
|
|
140
|
+
{ populate: ['person'] },
|
|
141
|
+
entityScope,
|
|
142
|
+
),
|
|
138
143
|
)
|
|
139
144
|
|
|
140
145
|
const personIds = links
|
|
@@ -255,7 +255,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
255
255
|
|
|
256
256
|
// Section action (for tabs that expose add/create buttons)
|
|
257
257
|
const handleSectionActionChange = React.useCallback((action: SectionAction | null) => {
|
|
258
|
-
setSectionAction(action)
|
|
258
|
+
setSectionAction((prev) => (action !== null ? action : prev))
|
|
259
259
|
}, [])
|
|
260
260
|
|
|
261
261
|
const handleSectionAction = React.useCallback(() => {
|
|
@@ -420,6 +420,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
420
420
|
peopleCount={data.counts?.people ?? 0}
|
|
421
421
|
dealsCount={dealCount}
|
|
422
422
|
activitiesCount={data.counts?.activities ?? 0}
|
|
423
|
+
sectionAction={sectionAction}
|
|
423
424
|
>
|
|
424
425
|
{activeTab === 'people' && (
|
|
425
426
|
<CompanyPeopleSection
|
|
@@ -59,7 +59,6 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
59
59
|
|
|
60
60
|
const formSchema = React.useMemo(() => createPersonEditSchema(), [])
|
|
61
61
|
const fields = React.useMemo(() => createPersonEditFields(t), [t])
|
|
62
|
-
const groups = React.useMemo(() => createPersonPersonalDataGroups(t), [t])
|
|
63
62
|
|
|
64
63
|
const [data, setData] = React.useState<PersonOverview | null>(null)
|
|
65
64
|
const [isLoading, setIsLoading] = React.useState(true)
|
|
@@ -101,6 +100,16 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
101
100
|
? data.person.displayName
|
|
102
101
|
: t('customers.people.list.deleteFallbackName', 'this person')
|
|
103
102
|
|
|
103
|
+
const personDisplayNameForGroups =
|
|
104
|
+
typeof data?.person?.displayName === 'string' && data.person.displayName.trim().length
|
|
105
|
+
? data.person.displayName.trim()
|
|
106
|
+
: null
|
|
107
|
+
|
|
108
|
+
const groups = React.useMemo(
|
|
109
|
+
() => createPersonPersonalDataGroups(t, { entityName: personDisplayNameForGroups }),
|
|
110
|
+
[t, personDisplayNameForGroups],
|
|
111
|
+
)
|
|
112
|
+
|
|
104
113
|
const zoneSections = React.useMemo<ZoneSectionDescriptor[]>(() => [
|
|
105
114
|
{ id: 'personalData', icon: User, label: t('customers.people.form.groups.personalData', 'Personal data') },
|
|
106
115
|
{ id: 'companyRole', icon: Building2, label: t('customers.people.form.groups.companyRole', 'Company & role') },
|
|
@@ -249,7 +258,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
249
258
|
|
|
250
259
|
// Section action (for tabs that expose add/create buttons)
|
|
251
260
|
const handleSectionActionChange = React.useCallback((action: SectionAction | null) => {
|
|
252
|
-
setSectionAction(action)
|
|
261
|
+
setSectionAction((prev) => (action !== null ? action : prev))
|
|
253
262
|
}, [])
|
|
254
263
|
|
|
255
264
|
React.useEffect(() => {
|
|
@@ -424,6 +433,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
424
433
|
dealsCount={dealCount}
|
|
425
434
|
companiesCount={companyCount}
|
|
426
435
|
tasksCount={todoCount}
|
|
436
|
+
sectionAction={sectionAction}
|
|
427
437
|
>
|
|
428
438
|
<div className="min-w-0">
|
|
429
439
|
{(() => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
CustomerInteraction,
|
|
23
23
|
CustomerTodoLink,
|
|
24
24
|
CustomerEntity,
|
|
25
|
+
CustomerPersonCompanyLink,
|
|
25
26
|
CustomerPersonProfile,
|
|
26
27
|
CustomerTagAssignment,
|
|
27
28
|
} from '../data/entities'
|
|
@@ -49,7 +50,7 @@ import {
|
|
|
49
50
|
} from '@open-mercato/shared/lib/commands/customFieldSnapshots'
|
|
50
51
|
import type { CrudIndexerConfig, CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
|
|
51
52
|
import { E } from '#generated/entities.ids.generated'
|
|
52
|
-
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
53
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
53
54
|
import { CUSTOMER_ENTITY_ID } from '../lib/customFieldRouting'
|
|
54
55
|
import { CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'
|
|
55
56
|
|
|
@@ -72,6 +73,41 @@ const companyCrudEvents: CrudEventsConfig<CustomerEntity> = {
|
|
|
72
73
|
}),
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
type CompanyDeleteBlockerCounts = {
|
|
77
|
+
personLinks: number
|
|
78
|
+
dealLinks: number
|
|
79
|
+
directPeople: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildCompanyHasDependentsError(
|
|
83
|
+
translate: (key: string, fallback?: string, params?: Record<string, string | number>) => string,
|
|
84
|
+
counts: CompanyDeleteBlockerCounts,
|
|
85
|
+
): CrudHttpError {
|
|
86
|
+
const blockers: string[] = []
|
|
87
|
+
if (counts.personLinks > 0) {
|
|
88
|
+
blockers.push(
|
|
89
|
+
translate('customers.companies.delete.blockers.persons', 'linked persons ({{count}})', { count: counts.personLinks }),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
if (counts.dealLinks > 0) {
|
|
93
|
+
blockers.push(
|
|
94
|
+
translate('customers.companies.delete.blockers.deals', 'linked deals ({{count}})', { count: counts.dealLinks }),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
if (counts.directPeople > 0) {
|
|
98
|
+
blockers.push(
|
|
99
|
+
translate('customers.companies.delete.blockers.directPeople', 'persons whose primary company is this one ({{count}})', { count: counts.directPeople }),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
const summary = blockers.join(', ')
|
|
103
|
+
const message = translate(
|
|
104
|
+
'customers.companies.delete.blocked',
|
|
105
|
+
'Cannot delete company: {{blockers}}. Please unlink or reassign first.',
|
|
106
|
+
{ blockers: summary },
|
|
107
|
+
)
|
|
108
|
+
return new CrudHttpError(422, { error: message, code: 'COMPANY_HAS_DEPENDENTS' })
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
function companyEntityIndexEntry(entity: CustomerEntity): QueryIndexEventEntry {
|
|
76
112
|
return {
|
|
77
113
|
entityType: E.customers.customer_entity,
|
|
@@ -781,28 +817,80 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
|
|
|
781
817
|
},
|
|
782
818
|
async execute(input, ctx) {
|
|
783
819
|
const id = requireId(input, 'Company id required')
|
|
784
|
-
const
|
|
785
|
-
const snapshot = await loadCompanySnapshot(
|
|
786
|
-
const entity = await
|
|
820
|
+
const baseEm = (ctx.container.resolve('em') as EntityManager).fork()
|
|
821
|
+
const snapshot = await loadCompanySnapshot(baseEm, id)
|
|
822
|
+
const entity = await baseEm.findOne(CustomerEntity, { id, deletedAt: null })
|
|
787
823
|
const record = assertFound(entity, 'Company not found')
|
|
788
824
|
ensureTenantScope(ctx, record.tenantId)
|
|
789
825
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
await em.nativeDelete(CustomerInteraction, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
795
|
-
await em.nativeDelete(CustomerTodoLink, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
796
|
-
await em.nativeDelete(CustomerCompanyProfile, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
797
|
-
await em.nativeDelete(CustomerAddress, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
798
|
-
await em.nativeDelete(CustomerComment, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
799
|
-
await em.nativeDelete(CustomerTagAssignment, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
800
|
-
if (profile) {
|
|
801
|
-
await em.nativeDelete(CustomFieldValue, { entityId: COMPANY_ENTITY_ID, recordId: profile.id })
|
|
826
|
+
|
|
827
|
+
const dependentScope = {
|
|
828
|
+
organizationId: record.organizationId,
|
|
829
|
+
tenantId: record.tenantId,
|
|
802
830
|
}
|
|
803
|
-
await
|
|
804
|
-
|
|
805
|
-
|
|
831
|
+
const personLinks = await baseEm.count(CustomerPersonCompanyLink, {
|
|
832
|
+
company: record,
|
|
833
|
+
deletedAt: null,
|
|
834
|
+
...dependentScope,
|
|
835
|
+
})
|
|
836
|
+
const dealLinks = await baseEm.count(CustomerDealCompanyLink, {
|
|
837
|
+
company: record,
|
|
838
|
+
})
|
|
839
|
+
const directPeople = await baseEm.count(CustomerPersonProfile, {
|
|
840
|
+
company: record,
|
|
841
|
+
...dependentScope,
|
|
842
|
+
})
|
|
843
|
+
if (personLinks > 0 || dealLinks > 0 || directPeople > 0) {
|
|
844
|
+
const { translate } = await resolveTranslations()
|
|
845
|
+
throw buildCompanyHasDependentsError(translate, { personLinks, dealLinks, directPeople })
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const profile = await baseEm.findOne(CustomerCompanyProfile, { entity: record })
|
|
849
|
+
|
|
850
|
+
await baseEm.transactional(async (em) => {
|
|
851
|
+
const recheckPersonLinks = await em.count(CustomerPersonCompanyLink, {
|
|
852
|
+
company: record,
|
|
853
|
+
deletedAt: null,
|
|
854
|
+
...dependentScope,
|
|
855
|
+
})
|
|
856
|
+
const recheckDealLinks = await em.count(CustomerDealCompanyLink, {
|
|
857
|
+
company: record,
|
|
858
|
+
})
|
|
859
|
+
const recheckDirectPeople = await em.count(CustomerPersonProfile, {
|
|
860
|
+
company: record,
|
|
861
|
+
...dependentScope,
|
|
862
|
+
})
|
|
863
|
+
if (recheckPersonLinks > 0 || recheckDealLinks > 0 || recheckDirectPeople > 0) {
|
|
864
|
+
const { translate } = await resolveTranslations()
|
|
865
|
+
throw buildCompanyHasDependentsError(translate, {
|
|
866
|
+
personLinks: recheckPersonLinks,
|
|
867
|
+
dealLinks: recheckDealLinks,
|
|
868
|
+
directPeople: recheckDirectPeople,
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
await em.nativeUpdate(CustomerPersonProfile, { company: record }, { company: null })
|
|
873
|
+
await em.nativeDelete(CustomerDealCompanyLink, { company: record })
|
|
874
|
+
await em.nativeDelete(CustomerActivity, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
875
|
+
await em.nativeDelete(CustomerInteraction, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
876
|
+
await em.nativeDelete(CustomerTodoLink, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
877
|
+
await em.nativeDelete(CustomerCompanyProfile, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
878
|
+
await em.nativeDelete(CustomerAddress, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
879
|
+
await em.nativeDelete(CustomerComment, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
880
|
+
await em.nativeDelete(CustomerTagAssignment, { entity: record, organizationId: record.organizationId, tenantId: record.tenantId })
|
|
881
|
+
if (profile) {
|
|
882
|
+
await em.nativeDelete(CustomFieldValue, { entityId: COMPANY_ENTITY_ID, recordId: profile.id })
|
|
883
|
+
}
|
|
884
|
+
await em.nativeDelete(CustomFieldValue, { entityId: CUSTOMER_ENTITY_ID, recordId: record.id })
|
|
885
|
+
const txEntity = await findOneWithDecryption(
|
|
886
|
+
em,
|
|
887
|
+
CustomerEntity,
|
|
888
|
+
{ id: record.id },
|
|
889
|
+
undefined,
|
|
890
|
+
{ tenantId: record.tenantId, organizationId: record.organizationId },
|
|
891
|
+
)
|
|
892
|
+
if (txEntity) em.remove(txEntity)
|
|
893
|
+
})
|
|
806
894
|
|
|
807
895
|
const indexDeletes: QueryIndexEventEntry[] = []
|
|
808
896
|
const memberUpserts: QueryIndexEventEntry[] = []
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import type { CrudIndexerConfig, CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
|
|
55
55
|
import { E } from '#generated/entities.ids.generated'
|
|
56
56
|
import { findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
57
|
+
import { deriveDisplayName, isDerivedDisplayName } from '../lib/displayName'
|
|
57
58
|
import {
|
|
58
59
|
loadPersonCompanyLinks,
|
|
59
60
|
summarizePersonCompanies,
|
|
@@ -523,7 +524,10 @@ const createPersonCommand: CommandHandler<PersonCreateInput, { entityId: string;
|
|
|
523
524
|
const timezone = normalizeOptionalString(parsed.timezone)
|
|
524
525
|
const linkedInUrl = normalizeOptionalString(parsed.linkedInUrl)
|
|
525
526
|
const twitterUrl = normalizeOptionalString(parsed.twitterUrl)
|
|
526
|
-
const
|
|
527
|
+
const displayNameInput = parsed.displayName?.trim() ?? ''
|
|
528
|
+
const displayName = displayNameInput.length > 0
|
|
529
|
+
? displayNameInput
|
|
530
|
+
: deriveDisplayName(firstName, lastName)
|
|
527
531
|
const nextInteractionName = parsed.nextInteraction?.name ? parsed.nextInteraction.name.trim() : null
|
|
528
532
|
const nextInteractionRefId = normalizeOptionalString(parsed.nextInteraction?.refId)
|
|
529
533
|
const nextInteractionIcon = normalizeOptionalString(parsed.nextInteraction?.icon)
|
|
@@ -715,6 +719,17 @@ const updatePersonCommand: CommandHandler<PersonUpdateInput, { entityId: string
|
|
|
715
719
|
}
|
|
716
720
|
}
|
|
717
721
|
|
|
722
|
+
if (
|
|
723
|
+
parsed.displayName === undefined
|
|
724
|
+
&& (parsed.firstName !== undefined || parsed.lastName !== undefined)
|
|
725
|
+
&& isDerivedDisplayName(record.displayName, profile.firstName, profile.lastName)
|
|
726
|
+
) {
|
|
727
|
+
const nextFirst = parsed.firstName !== undefined ? parsed.firstName : profile.firstName
|
|
728
|
+
const nextLast = parsed.lastName !== undefined ? parsed.lastName : profile.lastName
|
|
729
|
+
const derived = deriveDisplayName(nextFirst, nextLast)
|
|
730
|
+
if (derived.length > 0) parsed.displayName = derived
|
|
731
|
+
}
|
|
732
|
+
|
|
718
733
|
await withAtomicFlush(em, [
|
|
719
734
|
() => {
|
|
720
735
|
if (parsed.description !== undefined) record.description = normalizeOptionalString(parsed.description)
|
|
@@ -535,14 +535,15 @@ const deletePersonCompanyLinkCommand: CommandHandler<PersonCompanyLinkDeleteInpu
|
|
|
535
535
|
const profile = await requirePersonProfile(em, person)
|
|
536
536
|
const linkWasPrimary = link.isPrimary
|
|
537
537
|
|
|
538
|
+
const existingLinks = await loadPersonCompanyLinks(em, person)
|
|
539
|
+
const remainingLinks = existingLinks.filter((entry) => entry.id !== link.id)
|
|
540
|
+
|
|
538
541
|
await withAtomicFlush(em, [
|
|
539
542
|
() => {
|
|
540
543
|
link.isPrimary = false
|
|
541
544
|
link.deletedAt = new Date()
|
|
542
545
|
},
|
|
543
546
|
async () => {
|
|
544
|
-
const existingLinks = await loadPersonCompanyLinks(em, person)
|
|
545
|
-
const remainingLinks = existingLinks.filter((entry) => entry.id !== link.id)
|
|
546
547
|
if (linkWasPrimary) {
|
|
547
548
|
await promoteFallbackPrimaryLink(em, person, profile, remainingLinks, companyId)
|
|
548
549
|
} else if (profile.company && typeof profile.company !== 'string' && profile.company.id === companyId) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ExternalLink,
|
|
11
11
|
FileText,
|
|
12
12
|
Hash,
|
|
13
|
+
Link2Off,
|
|
13
14
|
MapPin,
|
|
14
15
|
TrendingUp,
|
|
15
16
|
Zap,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
Users,
|
|
18
19
|
} from 'lucide-react'
|
|
19
20
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
21
|
+
import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
|
|
20
22
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
21
23
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
22
24
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
@@ -67,6 +69,9 @@ type CompanyCardProps = {
|
|
|
67
69
|
temperatureMap?: CustomerDictionaryMap | undefined
|
|
68
70
|
renewalQuarterMap?: CustomerDictionaryMap | undefined
|
|
69
71
|
roleMap?: CustomerDictionaryMap | undefined
|
|
72
|
+
onUnlink?: () => void | Promise<void>
|
|
73
|
+
unlinkLabel?: string
|
|
74
|
+
unlinkDisabled?: boolean
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
function copyToClipboard(text: string, t: ReturnType<typeof useT>) {
|
|
@@ -208,8 +213,12 @@ export function CompanyCard({
|
|
|
208
213
|
temperatureMap,
|
|
209
214
|
renewalQuarterMap,
|
|
210
215
|
roleMap,
|
|
216
|
+
onUnlink,
|
|
217
|
+
unlinkLabel,
|
|
218
|
+
unlinkDisabled,
|
|
211
219
|
}: CompanyCardProps) {
|
|
212
220
|
const t = useT()
|
|
221
|
+
const resolvedUnlinkLabel = unlinkLabel ?? t('customers.people.detail.companies.unlinkAction', 'Unlink')
|
|
213
222
|
|
|
214
223
|
const hasBillingSection =
|
|
215
224
|
data.profile?.legalName ||
|
|
@@ -236,9 +245,12 @@ export function CompanyCard({
|
|
|
236
245
|
<div className="flex flex-wrap items-center gap-2">
|
|
237
246
|
<span className="break-words text-base font-bold">{data.displayName}</span>
|
|
238
247
|
{data.isPrimary && (
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
248
|
+
<StatusBadge
|
|
249
|
+
variant="info"
|
|
250
|
+
className="rounded-sm px-1.5 py-0 text-overline font-bold uppercase tracking-wider"
|
|
251
|
+
>
|
|
252
|
+
{t('customers.people.detail.companies.primaryBadge', 'Primary')}
|
|
253
|
+
</StatusBadge>
|
|
242
254
|
)}
|
|
243
255
|
</div>
|
|
244
256
|
{data.subtitle && (
|
|
@@ -246,7 +258,19 @@ export function CompanyCard({
|
|
|
246
258
|
)}
|
|
247
259
|
</div>
|
|
248
260
|
</div>
|
|
249
|
-
<div className="self-start sm:self-auto">
|
|
261
|
+
<div className="flex items-center gap-1 self-start sm:self-auto">
|
|
262
|
+
{onUnlink ? (
|
|
263
|
+
<IconButton
|
|
264
|
+
variant="ghost"
|
|
265
|
+
size="sm"
|
|
266
|
+
type="button"
|
|
267
|
+
aria-label={resolvedUnlinkLabel}
|
|
268
|
+
disabled={unlinkDisabled}
|
|
269
|
+
onClick={() => { void onUnlink() }}
|
|
270
|
+
>
|
|
271
|
+
<Link2Off className="size-4" />
|
|
272
|
+
</IconButton>
|
|
273
|
+
) : null}
|
|
250
274
|
<Link href={`/backend/customers/companies-v2/${data.companyId}`}>
|
|
251
275
|
<IconButton variant="ghost" size="sm" type="button">
|
|
252
276
|
<ExternalLink className="size-4" />
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
Clock,
|
|
12
12
|
History,
|
|
13
13
|
Paperclip,
|
|
14
|
+
Plus,
|
|
14
15
|
} from 'lucide-react'
|
|
16
|
+
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
15
17
|
|
|
16
18
|
export type CompanyTabId =
|
|
17
19
|
| 'people'
|
|
@@ -36,6 +38,7 @@ type CompanyDetailTabsProps = {
|
|
|
36
38
|
dealsCount?: number
|
|
37
39
|
activitiesCount?: number
|
|
38
40
|
filesCount?: number
|
|
41
|
+
sectionAction?: SectionAction | null
|
|
39
42
|
children: React.ReactNode
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -80,6 +83,7 @@ export function CompanyDetailTabs({
|
|
|
80
83
|
dealsCount = 0,
|
|
81
84
|
activitiesCount = 0,
|
|
82
85
|
filesCount = 0,
|
|
86
|
+
sectionAction = null,
|
|
83
87
|
children,
|
|
84
88
|
}: CompanyDetailTabsProps) {
|
|
85
89
|
const t = useT()
|
|
@@ -134,8 +138,8 @@ export function CompanyDetailTabs({
|
|
|
134
138
|
return (
|
|
135
139
|
<div>
|
|
136
140
|
{/* Tab navigation */}
|
|
137
|
-
<div className="border-b" role="tablist" aria-label={t('customers.companies.detail.tabs.label', 'Company detail sections')}>
|
|
138
|
-
<nav className="-mb-px flex gap-1 overflow-x-auto px-1">
|
|
141
|
+
<div className="flex items-end justify-between gap-2 border-b" role="tablist" aria-label={t('customers.companies.detail.tabs.label', 'Company detail sections')}>
|
|
142
|
+
<nav className="-mb-px flex flex-1 gap-1 overflow-x-auto px-1">
|
|
139
143
|
{allTabs.map((tab) => {
|
|
140
144
|
const isActive = activeTab === tab.id
|
|
141
145
|
return (
|
|
@@ -161,6 +165,18 @@ export function CompanyDetailTabs({
|
|
|
161
165
|
)
|
|
162
166
|
})}
|
|
163
167
|
</nav>
|
|
168
|
+
{sectionAction ? (
|
|
169
|
+
<Button
|
|
170
|
+
type="button"
|
|
171
|
+
size="sm"
|
|
172
|
+
onClick={sectionAction.onClick}
|
|
173
|
+
disabled={sectionAction.disabled}
|
|
174
|
+
className="mb-1.5 mr-1 shrink-0"
|
|
175
|
+
>
|
|
176
|
+
<Plus className="mr-1.5 h-4 w-4" />
|
|
177
|
+
{sectionAction.label}
|
|
178
|
+
</Button>
|
|
179
|
+
) : null}
|
|
164
180
|
</div>
|
|
165
181
|
|
|
166
182
|
{/* Tab content */}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '@open-mercato/shared/lib/browser/safeLocalStorage'
|
|
14
14
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
15
15
|
import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
|
|
16
|
+
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
16
17
|
import type { SectionAction, TabEmptyStateConfig, Translator } from './types'
|
|
17
18
|
import { CreatePersonDialog } from './CreatePersonDialog'
|
|
18
19
|
import { PersonCard } from './PersonCard'
|
|
@@ -319,6 +320,13 @@ export function CompanyPeopleSection({
|
|
|
319
320
|
void loadVisiblePeople()
|
|
320
321
|
}, [loadVisiblePeople])
|
|
321
322
|
|
|
323
|
+
useAppEvent('customers.person_company_link.deleted', (event) => {
|
|
324
|
+
const payload = event.payload as { companyEntityId?: string | null } | null | undefined
|
|
325
|
+
if (payload && payload.companyEntityId === companyId) {
|
|
326
|
+
void loadVisiblePeople()
|
|
327
|
+
}
|
|
328
|
+
}, [companyId, loadVisiblePeople])
|
|
329
|
+
|
|
322
330
|
React.useEffect(() => {
|
|
323
331
|
setListPage(1)
|
|
324
332
|
}, [searchQuery, sortMode])
|
|
@@ -520,9 +528,6 @@ export function CompanyPeopleSection({
|
|
|
520
528
|
companyId,
|
|
521
529
|
},
|
|
522
530
|
)
|
|
523
|
-
applyPeopleChange((current) => current.filter((entry) => entry.id !== personId))
|
|
524
|
-
setVisiblePeople((current) => current.filter((entry) => entry.id !== personId))
|
|
525
|
-
setListTotalCount((current) => Math.max(0, current - 1))
|
|
526
531
|
await loadVisiblePeople()
|
|
527
532
|
flash(
|
|
528
533
|
translate(
|
|
@@ -546,7 +551,6 @@ export function CompanyPeopleSection({
|
|
|
546
551
|
}
|
|
547
552
|
},
|
|
548
553
|
[
|
|
549
|
-
applyPeopleChange,
|
|
550
554
|
companyId,
|
|
551
555
|
loadVisiblePeople,
|
|
552
556
|
onLoadingChange,
|
|
@@ -7,6 +7,8 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
|
7
7
|
import { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
9
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
|
+
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
11
|
+
import { useAppEvent } from '@open-mercato/ui/backend/injection/useAppEvent'
|
|
10
12
|
import { CompanyCard, type EnrichedCompanyData } from './CompanyCard'
|
|
11
13
|
import { useCustomerDictionary } from './hooks/useCustomerDictionary'
|
|
12
14
|
import { LinkEntityDialog, type LinkEntityOption } from '../linking/LinkEntityDialog'
|
|
@@ -88,8 +90,10 @@ export function PersonCompaniesSection({
|
|
|
88
90
|
runGuardedMutation,
|
|
89
91
|
}: PersonCompaniesSectionProps) {
|
|
90
92
|
const t = useT()
|
|
93
|
+
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
91
94
|
const [items, setItems] = React.useState<EnrichedCompanyData[]>([])
|
|
92
95
|
const [loading, setLoading] = React.useState(true)
|
|
96
|
+
const [unlinkingId, setUnlinkingId] = React.useState<string | null>(null)
|
|
93
97
|
const [search, setSearch] = React.useState('')
|
|
94
98
|
const [sort, setSort] = React.useState<'name-asc' | 'name-desc' | 'recent'>('name-asc')
|
|
95
99
|
const [page, setPage] = React.useState(1)
|
|
@@ -309,6 +313,64 @@ export function PersonCompaniesSection({
|
|
|
309
313
|
[linkedCompanies, linkedPrimaryId, loadData, onChanged, personId, runWriteMutation, t],
|
|
310
314
|
)
|
|
311
315
|
|
|
316
|
+
const handleUnlink = React.useCallback(
|
|
317
|
+
async (companyId: string, displayName: string) => {
|
|
318
|
+
if (!companyId || unlinkingId) return
|
|
319
|
+
const confirmed = await confirm({
|
|
320
|
+
title: t('customers.people.detail.companies.unlinkConfirmTitle', 'Unlink company'),
|
|
321
|
+
description: t(
|
|
322
|
+
'customers.people.detail.companies.unlinkConfirm',
|
|
323
|
+
'Unlink {{company}} from {{person}}?',
|
|
324
|
+
{ company: displayName, person: _personName },
|
|
325
|
+
),
|
|
326
|
+
confirmText: t('customers.people.detail.companies.unlinkAction', 'Unlink'),
|
|
327
|
+
cancelText: t('customers.linking.actions.cancel', 'Cancel'),
|
|
328
|
+
})
|
|
329
|
+
if (!confirmed) return
|
|
330
|
+
setUnlinkingId(companyId)
|
|
331
|
+
try {
|
|
332
|
+
await runWriteMutation(
|
|
333
|
+
() =>
|
|
334
|
+
apiCallOrThrow(
|
|
335
|
+
`/api/customers/people/${encodeURIComponent(personId)}/companies/${encodeURIComponent(companyId)}`,
|
|
336
|
+
{ method: 'DELETE' },
|
|
337
|
+
{
|
|
338
|
+
errorMessage: t(
|
|
339
|
+
'customers.people.detail.companies.unlinkError',
|
|
340
|
+
'Failed to unlink company.',
|
|
341
|
+
),
|
|
342
|
+
},
|
|
343
|
+
),
|
|
344
|
+
{ companyId, personId, operation: 'unlinkPersonCompanyLink' },
|
|
345
|
+
)
|
|
346
|
+
await loadData({ showLoading: false })
|
|
347
|
+
await onChanged?.()
|
|
348
|
+
flash(
|
|
349
|
+
t('customers.people.detail.companies.unlinkSuccess', 'Company unlinked.'),
|
|
350
|
+
'success',
|
|
351
|
+
)
|
|
352
|
+
} catch (error) {
|
|
353
|
+
const message = error instanceof Error
|
|
354
|
+
? error.message
|
|
355
|
+
: t(
|
|
356
|
+
'customers.people.detail.companies.unlinkError',
|
|
357
|
+
'Failed to unlink company.',
|
|
358
|
+
)
|
|
359
|
+
flash(message, 'error')
|
|
360
|
+
} finally {
|
|
361
|
+
setUnlinkingId(null)
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
[_personName, confirm, loadData, onChanged, personId, runWriteMutation, t, unlinkingId],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
useAppEvent('customers.person_company_link.deleted', (event) => {
|
|
368
|
+
const payload = event.payload as { personEntityId?: string | null } | null | undefined
|
|
369
|
+
if (payload && payload.personEntityId === personId) {
|
|
370
|
+
void loadData({ showLoading: false })
|
|
371
|
+
}
|
|
372
|
+
}, [personId, loadData])
|
|
373
|
+
|
|
312
374
|
return (
|
|
313
375
|
<>
|
|
314
376
|
<div className="space-y-4">
|
|
@@ -399,6 +461,9 @@ export function PersonCompaniesSection({
|
|
|
399
461
|
temperatureMap={temperatureDict?.map}
|
|
400
462
|
renewalQuarterMap={renewalQuarterDict?.map}
|
|
401
463
|
roleMap={roleDict?.map}
|
|
464
|
+
onUnlink={() => handleUnlink(item.companyId, item.displayName)}
|
|
465
|
+
unlinkLabel={t('customers.people.detail.companies.unlinkAction', 'Unlink')}
|
|
466
|
+
unlinkDisabled={unlinkingId === item.companyId}
|
|
402
467
|
/>
|
|
403
468
|
))}
|
|
404
469
|
</div>
|
|
@@ -417,6 +482,7 @@ export function PersonCompaniesSection({
|
|
|
417
482
|
onConfirm={handleLinkConfirm}
|
|
418
483
|
runGuardedMutation={runWriteMutation}
|
|
419
484
|
/>
|
|
485
|
+
{ConfirmDialogElement}
|
|
420
486
|
</>
|
|
421
487
|
)
|
|
422
488
|
}
|
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
Check,
|
|
12
12
|
History,
|
|
13
13
|
Paperclip,
|
|
14
|
+
Plus,
|
|
14
15
|
} from 'lucide-react'
|
|
16
|
+
import type { SectionAction } from '@open-mercato/ui/backend/detail'
|
|
15
17
|
|
|
16
18
|
export type PersonTabId =
|
|
17
19
|
| 'activities'
|
|
@@ -38,6 +40,7 @@ type PersonDetailTabsProps = {
|
|
|
38
40
|
companiesCount?: number
|
|
39
41
|
tasksCount?: number
|
|
40
42
|
filesCount?: number
|
|
43
|
+
sectionAction?: SectionAction | null
|
|
41
44
|
children: React.ReactNode
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -74,6 +77,7 @@ export function PersonDetailTabs({
|
|
|
74
77
|
companiesCount = 0,
|
|
75
78
|
tasksCount = 0,
|
|
76
79
|
filesCount = 0,
|
|
80
|
+
sectionAction = null,
|
|
77
81
|
children,
|
|
78
82
|
}: PersonDetailTabsProps) {
|
|
79
83
|
const t = useT()
|
|
@@ -134,8 +138,8 @@ export function PersonDetailTabs({
|
|
|
134
138
|
return (
|
|
135
139
|
<div>
|
|
136
140
|
{/* Tab navigation — full width above both zones */}
|
|
137
|
-
<div className="border-b" role="tablist" aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}>
|
|
138
|
-
<nav className="-mb-px flex gap-1 overflow-x-auto px-1">
|
|
141
|
+
<div className="flex items-end justify-between gap-2 border-b" role="tablist" aria-label={t('customers.people.detail.tabs.label', 'Person detail sections')}>
|
|
142
|
+
<nav className="-mb-px flex flex-1 gap-1 overflow-x-auto px-1">
|
|
139
143
|
{allTabs.map((tab) => {
|
|
140
144
|
const isActive = activeTab === tab.id
|
|
141
145
|
return (
|
|
@@ -161,6 +165,18 @@ export function PersonDetailTabs({
|
|
|
161
165
|
)
|
|
162
166
|
})}
|
|
163
167
|
</nav>
|
|
168
|
+
{sectionAction ? (
|
|
169
|
+
<Button
|
|
170
|
+
type="button"
|
|
171
|
+
size="sm"
|
|
172
|
+
onClick={sectionAction.onClick}
|
|
173
|
+
disabled={sectionAction.disabled}
|
|
174
|
+
className="mb-1.5 mr-1 shrink-0"
|
|
175
|
+
>
|
|
176
|
+
<Plus className="mr-1.5 h-4 w-4" />
|
|
177
|
+
{sectionAction.label}
|
|
178
|
+
</Button>
|
|
179
|
+
) : null}
|
|
164
180
|
</div>
|
|
165
181
|
|
|
166
182
|
{/* Two-column content below tabs */}
|
|
@@ -431,14 +431,7 @@ export function TasksSection({
|
|
|
431
431
|
) : null}
|
|
432
432
|
|
|
433
433
|
{!isInitialLoading && !hasTasks ? (
|
|
434
|
-
<TabEmptyState
|
|
435
|
-
title={emptyState.title}
|
|
436
|
-
action={{
|
|
437
|
-
label: emptyState.actionLabel,
|
|
438
|
-
onClick: openCreateDialog,
|
|
439
|
-
disabled: isMutating || !entityId,
|
|
440
|
-
}}
|
|
441
|
-
/>
|
|
434
|
+
<TabEmptyState title={emptyState.title} />
|
|
442
435
|
) : null}
|
|
443
436
|
|
|
444
437
|
{!isInitialLoading && hasTasks ? (
|