@open-mercato/core 0.5.1-develop.2917.31ee9898e3 → 0.5.1-develop.2935.357c9db339
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/integrations/data/validators.js +1 -1
- package/dist/modules/integrations/data/validators.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/integrations/data/validators.ts +1 -1
|
@@ -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 ? (
|
|
@@ -7,6 +7,7 @@ import { Check, Pencil, Plus, Settings } from 'lucide-react'
|
|
|
7
7
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
8
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { deriveDisplayName, isDerivedDisplayName } from '../lib/displayName'
|
|
10
11
|
import {
|
|
11
12
|
Dialog,
|
|
12
13
|
DialogContent,
|
|
@@ -703,16 +704,18 @@ export const createDisplayNameSection = (t: Translator) =>
|
|
|
703
704
|
function DisplayNameSection({ values, setValue, errors }: CrudFormGroupComponentProps) {
|
|
704
705
|
const [editing, setEditing] = React.useState(false)
|
|
705
706
|
const [manualOverride, setManualOverride] = React.useState(() => {
|
|
706
|
-
const current = typeof values.displayName === 'string' ? values.displayName
|
|
707
|
-
|
|
707
|
+
const current = typeof values.displayName === 'string' ? values.displayName : ''
|
|
708
|
+
const firstInit = typeof values.firstName === 'string' ? values.firstName : ''
|
|
709
|
+
const lastInit = typeof values.lastName === 'string' ? values.lastName : ''
|
|
710
|
+
// Sticky-manual: treat as user-customized only when the persisted display name
|
|
711
|
+
// doesn't match the first+last derivation (matches the server-side rule in
|
|
712
|
+
// updatePersonCommand). Empty values are considered derived.
|
|
713
|
+
return !isDerivedDisplayName(current, firstInit, lastInit)
|
|
708
714
|
})
|
|
709
715
|
|
|
710
716
|
const first = typeof values.firstName === 'string' ? values.firstName.trim() : ''
|
|
711
717
|
const last = typeof values.lastName === 'string' ? values.lastName.trim() : ''
|
|
712
|
-
const derived = React.useMemo(() =>
|
|
713
|
-
const parts = [first, last].filter((part) => !!part)
|
|
714
|
-
return parts.join(' ').trim()
|
|
715
|
-
}, [first, last])
|
|
718
|
+
const derived = React.useMemo(() => deriveDisplayName(first, last), [first, last])
|
|
716
719
|
|
|
717
720
|
React.useEffect(() => {
|
|
718
721
|
if (!manualOverride) {
|
|
@@ -1684,40 +1687,56 @@ export const createPersonEditGroups = (t: Translator): CrudFormGroup[] => [
|
|
|
1684
1687
|
* Groups for the Person v2 "Dane osobowe" Figma layout (SPEC-048 mockup).
|
|
1685
1688
|
* All groups in column 1 (Zone 1). Notes handled separately in Zone 2 tabs.
|
|
1686
1689
|
*/
|
|
1687
|
-
export const createPersonPersonalDataGroups = (
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1690
|
+
export const createPersonPersonalDataGroups = (
|
|
1691
|
+
t: Translator,
|
|
1692
|
+
options?: { entityName?: string | null },
|
|
1693
|
+
): CrudFormGroup[] => {
|
|
1694
|
+
const entityName = options?.entityName?.trim() || null
|
|
1695
|
+
const rolesTitle = entityName
|
|
1696
|
+
? t('customers.roles.groupTitle.person', 'My roles with {{name}}', { name: entityName })
|
|
1697
|
+
: t('customers.people.form.groups.roles', 'My roles')
|
|
1698
|
+
return [
|
|
1699
|
+
{
|
|
1700
|
+
id: 'personalDataDisplay',
|
|
1701
|
+
title: t('customers.people.form.groups.displayName', 'Display name'),
|
|
1702
|
+
column: 1,
|
|
1703
|
+
bare: true,
|
|
1704
|
+
component: createDisplayNameSection(t),
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
id: 'personalData',
|
|
1708
|
+
title: t('customers.people.form.groups.personalData', 'Personal data'),
|
|
1709
|
+
column: 1,
|
|
1710
|
+
fields: ['firstName', 'lastName', 'jobTitle', 'primaryEmail', 'primaryPhone'],
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
id: 'companyRole',
|
|
1714
|
+
title: t('customers.people.form.groups.companyRole', 'Company & role'),
|
|
1715
|
+
column: 1,
|
|
1716
|
+
fields: ['companyEntityId', 'status', 'lifecycleStage', 'source'],
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
id: 'customFields',
|
|
1720
|
+
title: t('customers.people.form.groups.customAttributes', 'Custom attributes'),
|
|
1721
|
+
column: 1,
|
|
1722
|
+
kind: 'customFields',
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
id: 'roles',
|
|
1726
|
+
title: rolesTitle,
|
|
1727
|
+
column: 1,
|
|
1728
|
+
component: ({ values }: CrudFormGroupComponentProps) => (
|
|
1729
|
+
values.id ? (
|
|
1730
|
+
<RolesSection
|
|
1731
|
+
entityType="person"
|
|
1732
|
+
entityId={values.id as string}
|
|
1733
|
+
entityName={typeof values.displayName === 'string' ? values.displayName : null}
|
|
1734
|
+
/>
|
|
1735
|
+
) : null
|
|
1736
|
+
),
|
|
1737
|
+
},
|
|
1738
|
+
]
|
|
1739
|
+
}
|
|
1721
1740
|
|
|
1722
1741
|
// ---------------------------------------------------------------------------
|
|
1723
1742
|
// Edit-mode payload builders
|
|
@@ -75,9 +75,9 @@ const events = [
|
|
|
75
75
|
{ id: 'customers.label_assignment.deleted', label: 'Label Unassigned', entity: 'label_assignment', category: 'crud' },
|
|
76
76
|
|
|
77
77
|
// Person-Company Links
|
|
78
|
-
{ id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud' },
|
|
79
|
-
{ id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud' },
|
|
80
|
-
{ id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud' },
|
|
78
|
+
{ id: 'customers.person_company_link.created', label: 'Person Linked To Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
|
|
79
|
+
{ id: 'customers.person_company_link.updated', label: 'Person-Company Link Updated', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
|
|
80
|
+
{ id: 'customers.person_company_link.deleted', label: 'Person Unlinked From Company', entity: 'person_company_link', category: 'crud', clientBroadcast: true },
|
|
81
81
|
] as const
|
|
82
82
|
|
|
83
83
|
export const eventsConfig = createModuleEvents({
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
"customers.companies.dashboard.seeAllActivity": "Alle {{count}} Aktivitäten anzeigen",
|
|
196
196
|
"customers.companies.dashboard.showAll": "Show all",
|
|
197
197
|
"customers.companies.dashboard.upcomingMeetings": "Anstehende Meetings",
|
|
198
|
+
"customers.companies.delete.blocked": "Unternehmen kann nicht gelöscht werden: {{blockers}}. Bitte zuerst Verknüpfungen lösen oder neu zuweisen.",
|
|
199
|
+
"customers.companies.delete.blockers.deals": "verknüpfte Deals ({{count}})",
|
|
200
|
+
"customers.companies.delete.blockers.directPeople": "Personen, deren primäres Unternehmen dies ist ({{count}})",
|
|
201
|
+
"customers.companies.delete.blockers.persons": "verknüpfte Personen ({{count}})",
|
|
198
202
|
"customers.companies.detail.actions.addDeal": "Deal hinzufügen",
|
|
199
203
|
"customers.companies.detail.actions.backToList": "Zurück zu Unternehmen",
|
|
200
204
|
"customers.companies.detail.actions.cancel": "Abbrechen",
|
|
@@ -1189,6 +1193,11 @@
|
|
|
1189
1193
|
"customers.people.detail.companies.sortRecent": "Sort: Recently active",
|
|
1190
1194
|
"customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
|
|
1191
1195
|
"customers.people.detail.companies.summary": "{{count}} verknüpfte Firmen",
|
|
1196
|
+
"customers.people.detail.companies.unlinkAction": "Verknüpfung lösen",
|
|
1197
|
+
"customers.people.detail.companies.unlinkConfirm": "Verknüpfung von {{company}} mit {{person}} lösen?",
|
|
1198
|
+
"customers.people.detail.companies.unlinkConfirmTitle": "Firma entkoppeln",
|
|
1199
|
+
"customers.people.detail.companies.unlinkError": "Verknüpfung der Firma konnte nicht gelöst werden.",
|
|
1200
|
+
"customers.people.detail.companies.unlinkSuccess": "Firma entkoppelt.",
|
|
1192
1201
|
"customers.people.detail.company.current": "{{company}}",
|
|
1193
1202
|
"customers.people.detail.company.empty": "Kein Unternehmen zugewiesen",
|
|
1194
1203
|
"customers.people.detail.company.label": "Unternehmen",
|
|
@@ -1530,6 +1539,7 @@
|
|
|
1530
1539
|
"customers.people.form.groups.custom": "Individuelle Felder",
|
|
1531
1540
|
"customers.people.form.groups.customAttributes": "Custom attributes",
|
|
1532
1541
|
"customers.people.form.groups.details": "Details",
|
|
1542
|
+
"customers.people.form.groups.displayName": "Anzeigename",
|
|
1533
1543
|
"customers.people.form.groups.notes": "Notizen",
|
|
1534
1544
|
"customers.people.form.groups.personalData": "Personal data",
|
|
1535
1545
|
"customers.people.form.groups.roles": "My roles",
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
"customers.companies.dashboard.seeAllActivity": "See all {{count}} activities",
|
|
196
196
|
"customers.companies.dashboard.showAll": "Show all",
|
|
197
197
|
"customers.companies.dashboard.upcomingMeetings": "Upcoming meetings",
|
|
198
|
+
"customers.companies.delete.blocked": "Cannot delete company: {{blockers}}. Please unlink or reassign first.",
|
|
199
|
+
"customers.companies.delete.blockers.deals": "linked deals ({{count}})",
|
|
200
|
+
"customers.companies.delete.blockers.directPeople": "persons whose primary company is this one ({{count}})",
|
|
201
|
+
"customers.companies.delete.blockers.persons": "linked persons ({{count}})",
|
|
198
202
|
"customers.companies.detail.actions.addDeal": "Add deal",
|
|
199
203
|
"customers.companies.detail.actions.backToList": "Back to companies",
|
|
200
204
|
"customers.companies.detail.actions.cancel": "Cancel",
|
|
@@ -1189,6 +1193,11 @@
|
|
|
1189
1193
|
"customers.people.detail.companies.sortRecent": "Sort: Recently active",
|
|
1190
1194
|
"customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
|
|
1191
1195
|
"customers.people.detail.companies.summary": "{{count}} linked companies",
|
|
1196
|
+
"customers.people.detail.companies.unlinkAction": "Unlink",
|
|
1197
|
+
"customers.people.detail.companies.unlinkConfirm": "Unlink {{company}} from {{person}}?",
|
|
1198
|
+
"customers.people.detail.companies.unlinkConfirmTitle": "Unlink company",
|
|
1199
|
+
"customers.people.detail.companies.unlinkError": "Failed to unlink company.",
|
|
1200
|
+
"customers.people.detail.companies.unlinkSuccess": "Company unlinked.",
|
|
1192
1201
|
"customers.people.detail.company.current": "{{company}}",
|
|
1193
1202
|
"customers.people.detail.company.empty": "No company assigned",
|
|
1194
1203
|
"customers.people.detail.company.label": "Company",
|
|
@@ -1530,6 +1539,7 @@
|
|
|
1530
1539
|
"customers.people.form.groups.custom": "Custom fields",
|
|
1531
1540
|
"customers.people.form.groups.customAttributes": "Custom attributes",
|
|
1532
1541
|
"customers.people.form.groups.details": "Details",
|
|
1542
|
+
"customers.people.form.groups.displayName": "Display name",
|
|
1533
1543
|
"customers.people.form.groups.notes": "Notes",
|
|
1534
1544
|
"customers.people.form.groups.personalData": "Personal data",
|
|
1535
1545
|
"customers.people.form.groups.roles": "My roles",
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
"customers.companies.dashboard.seeAllActivity": "Ver todas las {{count}} actividades",
|
|
196
196
|
"customers.companies.dashboard.showAll": "Show all",
|
|
197
197
|
"customers.companies.dashboard.upcomingMeetings": "Próximas reuniones",
|
|
198
|
+
"customers.companies.delete.blocked": "No se puede eliminar la empresa: {{blockers}}. Por favor, desvincula o reasigna primero.",
|
|
199
|
+
"customers.companies.delete.blockers.deals": "oportunidades vinculadas ({{count}})",
|
|
200
|
+
"customers.companies.delete.blockers.directPeople": "personas cuya empresa principal es esta ({{count}})",
|
|
201
|
+
"customers.companies.delete.blockers.persons": "personas vinculadas ({{count}})",
|
|
198
202
|
"customers.companies.detail.actions.addDeal": "Agregar oportunidad",
|
|
199
203
|
"customers.companies.detail.actions.backToList": "Volver a empresas",
|
|
200
204
|
"customers.companies.detail.actions.cancel": "Cancelar",
|
|
@@ -1189,6 +1193,11 @@
|
|
|
1189
1193
|
"customers.people.detail.companies.sortRecent": "Sort: Recently active",
|
|
1190
1194
|
"customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
|
|
1191
1195
|
"customers.people.detail.companies.summary": "{{count}} empresas vinculadas",
|
|
1196
|
+
"customers.people.detail.companies.unlinkAction": "Desvincular",
|
|
1197
|
+
"customers.people.detail.companies.unlinkConfirm": "¿Desvincular {{company}} de {{person}}?",
|
|
1198
|
+
"customers.people.detail.companies.unlinkConfirmTitle": "Desvincular empresa",
|
|
1199
|
+
"customers.people.detail.companies.unlinkError": "No se pudo desvincular la empresa.",
|
|
1200
|
+
"customers.people.detail.companies.unlinkSuccess": "Empresa desvinculada.",
|
|
1192
1201
|
"customers.people.detail.company.current": "{{company}}",
|
|
1193
1202
|
"customers.people.detail.company.empty": "Ninguna empresa asignada",
|
|
1194
1203
|
"customers.people.detail.company.label": "Empresa",
|
|
@@ -1530,6 +1539,7 @@
|
|
|
1530
1539
|
"customers.people.form.groups.custom": "Campos personalizados",
|
|
1531
1540
|
"customers.people.form.groups.customAttributes": "Custom attributes",
|
|
1532
1541
|
"customers.people.form.groups.details": "Detalles",
|
|
1542
|
+
"customers.people.form.groups.displayName": "Nombre para mostrar",
|
|
1533
1543
|
"customers.people.form.groups.notes": "Notas",
|
|
1534
1544
|
"customers.people.form.groups.personalData": "Personal data",
|
|
1535
1545
|
"customers.people.form.groups.roles": "My roles",
|
|
@@ -195,6 +195,10 @@
|
|
|
195
195
|
"customers.companies.dashboard.seeAllActivity": "Zobacz wszystkie {{count}} aktywności",
|
|
196
196
|
"customers.companies.dashboard.showAll": "Show all",
|
|
197
197
|
"customers.companies.dashboard.upcomingMeetings": "Nadchodzące spotkania",
|
|
198
|
+
"customers.companies.delete.blocked": "Nie można usunąć firmy: {{blockers}}. Najpierw odłącz lub przepisz powiązania.",
|
|
199
|
+
"customers.companies.delete.blockers.deals": "powiązane szanse ({{count}})",
|
|
200
|
+
"customers.companies.delete.blockers.directPeople": "osoby, dla których ta firma jest podstawowa ({{count}})",
|
|
201
|
+
"customers.companies.delete.blockers.persons": "powiązane osoby ({{count}})",
|
|
198
202
|
"customers.companies.detail.actions.addDeal": "Dodaj szansę",
|
|
199
203
|
"customers.companies.detail.actions.backToList": "Powrót do listy firm",
|
|
200
204
|
"customers.companies.detail.actions.cancel": "Anuluj",
|
|
@@ -1189,6 +1193,11 @@
|
|
|
1189
1193
|
"customers.people.detail.companies.sortRecent": "Sort: Recently active",
|
|
1190
1194
|
"customers.people.detail.companies.subtitle": "Link one or more companies and choose the primary relationship for this person.",
|
|
1191
1195
|
"customers.people.detail.companies.summary": "{{count}} powiązanych firm",
|
|
1196
|
+
"customers.people.detail.companies.unlinkAction": "Odłącz",
|
|
1197
|
+
"customers.people.detail.companies.unlinkConfirm": "Odłączyć firmę {{company}} od {{person}}?",
|
|
1198
|
+
"customers.people.detail.companies.unlinkConfirmTitle": "Odłącz firmę",
|
|
1199
|
+
"customers.people.detail.companies.unlinkError": "Nie udało się odłączyć firmy.",
|
|
1200
|
+
"customers.people.detail.companies.unlinkSuccess": "Firma odłączona.",
|
|
1192
1201
|
"customers.people.detail.company.current": "{{company}}",
|
|
1193
1202
|
"customers.people.detail.company.empty": "Brak przypisanej firmy",
|
|
1194
1203
|
"customers.people.detail.company.label": "Firma",
|
|
@@ -1530,6 +1539,7 @@
|
|
|
1530
1539
|
"customers.people.form.groups.custom": "Pola niestandardowe",
|
|
1531
1540
|
"customers.people.form.groups.customAttributes": "Atrybuty niestandardowe",
|
|
1532
1541
|
"customers.people.form.groups.details": "Szczegóły",
|
|
1542
|
+
"customers.people.form.groups.displayName": "Nazwa wyświetlana",
|
|
1533
1543
|
"customers.people.form.groups.notes": "Notatki",
|
|
1534
1544
|
"customers.people.form.groups.personalData": "Dane osobowe",
|
|
1535
1545
|
"customers.people.form.groups.roles": "Moje role",
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
export function deriveDisplayName(
|
|
2
|
+
firstName: string | null | undefined,
|
|
3
|
+
lastName: string | null | undefined,
|
|
4
|
+
): string {
|
|
5
|
+
const first = (firstName ?? '').trim()
|
|
6
|
+
const last = (lastName ?? '').trim()
|
|
7
|
+
return [first, last].filter((part) => part.length > 0).join(' ').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isDerivedDisplayName(
|
|
11
|
+
current: string | null | undefined,
|
|
12
|
+
firstName: string | null | undefined,
|
|
13
|
+
lastName: string | null | undefined,
|
|
14
|
+
): boolean {
|
|
15
|
+
const trimmed = (current ?? '').trim()
|
|
16
|
+
if (trimmed.length === 0) return true
|
|
17
|
+
return trimmed === deriveDisplayName(firstName, lastName)
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export function deriveDisplayNameFromEmail(email: string | null | undefined): string | null {
|
|
2
21
|
if (typeof email !== 'string') return null
|
|
3
22
|
const trimmed = email.trim()
|