@open-mercato/core 0.5.1-develop.2800.bfe2178a4f → 0.5.1-develop.2851.2854b4507f
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/generated/entities/action_log/index.js +4 -0
- package/dist/generated/entities/action_log/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/audit_logs/data/entities.js +10 -1
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/data/validators.js +2 -0
- package/dist/modules/audit_logs/data/validators.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260423202109.js +15 -0
- package/dist/modules/audit_logs/migrations/Migration20260423202109.js.map +7 -0
- package/dist/modules/audit_logs/services/accessLogService.js +3 -2
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +13 -2
- package/dist/modules/audit_logs/services/actionLogService.js.map +3 -3
- package/dist/modules/auth/cli.js.map +2 -2
- package/dist/modules/customers/api/entity-roles-factory.js +3 -18
- package/dist/modules/customers/api/entity-roles-factory.js.map +2 -2
- package/dist/modules/customers/api/interactions/cancel/route.js +7 -2
- package/dist/modules/customers/api/interactions/cancel/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/complete/route.js +7 -2
- package/dist/modules/customers/api/interactions/complete/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +45 -44
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/commands/comments.js +6 -0
- package/dist/modules/customers/commands/comments.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +41 -13
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js +30 -0
- package/dist/modules/customers/components/detail/CompanyDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailHeader.js +32 -0
- package/dist/modules/customers/components/detail/DealDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/DealWonPopup.js +2 -2
- package/dist/modules/customers/components/detail/DealWonPopup.js.map +2 -2
- package/dist/modules/customers/components/detail/InlineActivityComposer.js +62 -6
- package/dist/modules/customers/components/detail/InlineActivityComposer.js.map +2 -2
- package/dist/modules/customers/components/detail/ObjectHistoryButton.js +39 -0
- package/dist/modules/customers/components/detail/ObjectHistoryButton.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js +30 -0
- package/dist/modules/customers/components/detail/PersonDetailHeader.js.map +2 -2
- package/dist/modules/customers/components/detail/RolesSection.js +14 -4
- package/dist/modules/customers/components/detail/RolesSection.js.map +3 -3
- package/dist/modules/customers/components/formConfig.js +16 -2
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/lib/displayName.js +15 -0
- package/dist/modules/customers/lib/displayName.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -2
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/operationMetadata.js +21 -0
- package/dist/modules/customers/lib/operationMetadata.js.map +7 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js +106 -107
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js +235 -0
- package/dist/modules/messages/components/useMessagesInboxBulkActions.js.map +7 -0
- package/generated/entities/action_log/index.ts +2 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +3 -3
- package/src/modules/audit_logs/data/entities.ts +7 -0
- package/src/modules/audit_logs/data/validators.ts +2 -0
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +51 -5
- package/src/modules/audit_logs/migrations/Migration20260423202109.ts +15 -0
- package/src/modules/audit_logs/services/accessLogService.ts +1 -3
- package/src/modules/audit_logs/services/actionLogService.ts +11 -6
- package/src/modules/auth/cli.ts +1 -1
- package/src/modules/customers/api/entity-roles-factory.ts +3 -23
- package/src/modules/customers/api/interactions/cancel/route.ts +7 -2
- package/src/modules/customers/api/interactions/complete/route.ts +7 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +48 -44
- package/src/modules/customers/commands/comments.ts +6 -0
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +37 -9
- package/src/modules/customers/components/detail/CompanyDetailHeader.tsx +25 -0
- package/src/modules/customers/components/detail/DealDetailHeader.tsx +29 -0
- package/src/modules/customers/components/detail/DealWonPopup.tsx +2 -2
- package/src/modules/customers/components/detail/InlineActivityComposer.tsx +65 -6
- package/src/modules/customers/components/detail/ObjectHistoryButton.tsx +47 -0
- package/src/modules/customers/components/detail/PersonDetailHeader.tsx +25 -0
- package/src/modules/customers/components/detail/RolesSection.tsx +20 -1
- package/src/modules/customers/components/formConfig.tsx +14 -2
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +12 -0
- package/src/modules/customers/i18n/es.json +13 -1
- package/src/modules/customers/i18n/pl.json +13 -1
- package/src/modules/customers/lib/displayName.ts +16 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -7
- package/src/modules/customers/lib/operationMetadata.ts +38 -0
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +17 -29
- package/src/modules/messages/components/useMessagesInboxBulkActions.ts +324 -0
- package/src/modules/messages/i18n/de.json +8 -0
- package/src/modules/messages/i18n/en.json +8 -0
- package/src/modules/messages/i18n/es.json +8 -0
- package/src/modules/messages/i18n/pl.json +8 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
validateCrudMutationGuard,
|
|
15
15
|
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
16
16
|
import { resolveAuthActorId } from '../../../lib/interactionRequestContext'
|
|
17
|
+
import { withOperationMetadata } from '../../../lib/operationMetadata'
|
|
17
18
|
|
|
18
19
|
export const metadata = {
|
|
19
20
|
POST: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },
|
|
@@ -56,7 +57,7 @@ export async function POST(req: Request) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const commandBus = ctx.container.resolve('commandBus') as CommandBus
|
|
59
|
-
await commandBus.execute<InteractionCompleteInput, { interactionId: string }>(
|
|
60
|
+
const { logEntry } = await commandBus.execute<InteractionCompleteInput, { interactionId: string }>(
|
|
60
61
|
'customers.interactions.complete',
|
|
61
62
|
{ input: parsed, ctx },
|
|
62
63
|
)
|
|
@@ -73,7 +74,11 @@ export async function POST(req: Request) {
|
|
|
73
74
|
metadata: guardResult.metadata ?? null,
|
|
74
75
|
})
|
|
75
76
|
}
|
|
76
|
-
return
|
|
77
|
+
return withOperationMetadata(
|
|
78
|
+
NextResponse.json({ ok: true }),
|
|
79
|
+
logEntry,
|
|
80
|
+
{ resourceKind: 'customers.interaction', resourceId: parsed.id },
|
|
81
|
+
)
|
|
77
82
|
} catch (err) {
|
|
78
83
|
if (err instanceof CrudHttpError) {
|
|
79
84
|
return NextResponse.json(err.body, { status: err.status })
|
|
@@ -9,7 +9,7 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
|
9
9
|
import { DataTable, type DataTableExportFormat, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
|
|
10
10
|
import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
|
|
11
11
|
import type { AdvancedFilterState } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
12
|
-
import { serializeAdvancedFilter } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
12
|
+
import { deserializeAdvancedFilter, serializeAdvancedFilter } from '@open-mercato/shared/lib/query/advanced-filter'
|
|
13
13
|
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
14
14
|
import { buildCrudExportUrl, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
15
15
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
@@ -292,7 +292,16 @@ export default function CustomersDealsPage() {
|
|
|
292
292
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
293
293
|
const [pendingDeleteId, setPendingDeleteId] = React.useState<string | null>(null)
|
|
294
294
|
const [filterValues, setFilterValues] = React.useState<FilterValues>({})
|
|
295
|
-
const [advancedFilterState, setAdvancedFilterState] = React.useState<AdvancedFilterState>(
|
|
295
|
+
const [advancedFilterState, setAdvancedFilterState] = React.useState<AdvancedFilterState>(() => {
|
|
296
|
+
const params = searchParams
|
|
297
|
+
if (!params) return { logic: 'and', conditions: [] }
|
|
298
|
+
const record: Record<string, string> = {}
|
|
299
|
+
params.forEach((value, key) => {
|
|
300
|
+
if (key.startsWith('filter[')) record[key] = value
|
|
301
|
+
})
|
|
302
|
+
const hydrated = deserializeAdvancedFilter(record)
|
|
303
|
+
return hydrated ?? { logic: 'and', conditions: [] }
|
|
304
|
+
})
|
|
296
305
|
const [cacheStatus, setCacheStatus] = React.useState<'hit' | 'miss' | null>(null)
|
|
297
306
|
|
|
298
307
|
const initialPersonIds = React.useMemo(
|
|
@@ -353,7 +362,6 @@ export default function CustomersDealsPage() {
|
|
|
353
362
|
const base = buildPersonLabel(record)
|
|
354
363
|
let previousLabel = idToLabel[record.id]
|
|
355
364
|
if (previousLabel) {
|
|
356
|
-
// remove previous label before reassigning
|
|
357
365
|
delete labelToId[previousLabel]
|
|
358
366
|
occupied.delete(previousLabel)
|
|
359
367
|
}
|
|
@@ -511,10 +519,9 @@ export default function CustomersDealsPage() {
|
|
|
511
519
|
})
|
|
512
520
|
}, [scopeVersion, reloadToken])
|
|
513
521
|
|
|
514
|
-
const
|
|
522
|
+
const syncFilterIds = React.useCallback((
|
|
515
523
|
key: 'people' | 'companies',
|
|
516
524
|
ids: string[],
|
|
517
|
-
idToLabel: Record<string, string>,
|
|
518
525
|
) => {
|
|
519
526
|
setFilterValues((prev) => {
|
|
520
527
|
const current = Array.isArray(prev[key]) ? (prev[key] as string[]) : []
|
|
@@ -524,24 +531,18 @@ export default function CustomersDealsPage() {
|
|
|
524
531
|
delete next[key]
|
|
525
532
|
return next
|
|
526
533
|
}
|
|
527
|
-
|
|
528
|
-
ids
|
|
529
|
-
const label = idToLabel[id]
|
|
530
|
-
if (label && !labels.includes(label)) labels.push(label)
|
|
531
|
-
})
|
|
532
|
-
if (labels.length < ids.length) return prev
|
|
533
|
-
if (arraysEqual(current, labels)) return prev
|
|
534
|
-
return { ...prev, [key]: labels }
|
|
534
|
+
if (arraysEqual(current, ids)) return prev
|
|
535
|
+
return { ...prev, [key]: [...ids] }
|
|
535
536
|
})
|
|
536
537
|
}, [])
|
|
537
538
|
|
|
538
539
|
React.useEffect(() => {
|
|
539
|
-
|
|
540
|
-
}, [selectedPersonIds,
|
|
540
|
+
syncFilterIds('people', selectedPersonIds)
|
|
541
|
+
}, [selectedPersonIds, syncFilterIds])
|
|
541
542
|
|
|
542
543
|
React.useEffect(() => {
|
|
543
|
-
|
|
544
|
-
}, [selectedCompanyIds,
|
|
544
|
+
syncFilterIds('companies', selectedCompanyIds)
|
|
545
|
+
}, [selectedCompanyIds, syncFilterIds])
|
|
545
546
|
|
|
546
547
|
const handleSearchChange = React.useCallback((value: string) => {
|
|
547
548
|
setSearch(value.trim())
|
|
@@ -550,39 +551,34 @@ export default function CustomersDealsPage() {
|
|
|
550
551
|
|
|
551
552
|
const handleFiltersApply = React.useCallback((values: FilterValues) => {
|
|
552
553
|
const next: FilterValues = { ...values }
|
|
554
|
+
|
|
553
555
|
const rawPeople = Array.isArray(values.people) ? (values.people as string[]) : []
|
|
554
|
-
const nextPersonIds
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
556
|
+
const nextPersonIds = Array.from(
|
|
557
|
+
new Set(
|
|
558
|
+
rawPeople
|
|
559
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
560
|
+
.filter((value) => value.length > 0 && isUuid(value)),
|
|
561
|
+
),
|
|
562
|
+
)
|
|
561
563
|
setSelectedPersonIds(nextPersonIds)
|
|
562
|
-
if (nextPersonIds.length)
|
|
563
|
-
|
|
564
|
-
} else {
|
|
565
|
-
delete next.people
|
|
566
|
-
}
|
|
564
|
+
if (nextPersonIds.length) next.people = nextPersonIds
|
|
565
|
+
else delete next.people
|
|
567
566
|
|
|
568
567
|
const rawCompanies = Array.isArray(values.companies) ? (values.companies as string[]) : []
|
|
569
|
-
const nextCompanyIds
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
568
|
+
const nextCompanyIds = Array.from(
|
|
569
|
+
new Set(
|
|
570
|
+
rawCompanies
|
|
571
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
572
|
+
.filter((value) => value.length > 0 && isUuid(value)),
|
|
573
|
+
),
|
|
574
|
+
)
|
|
576
575
|
setSelectedCompanyIds(nextCompanyIds)
|
|
577
|
-
if (nextCompanyIds.length)
|
|
578
|
-
|
|
579
|
-
} else {
|
|
580
|
-
delete next.companies
|
|
581
|
-
}
|
|
576
|
+
if (nextCompanyIds.length) next.companies = nextCompanyIds
|
|
577
|
+
else delete next.companies
|
|
582
578
|
|
|
583
579
|
setFilterValues(next)
|
|
584
580
|
setPage(1)
|
|
585
|
-
}, [
|
|
581
|
+
}, [])
|
|
586
582
|
|
|
587
583
|
const handleFiltersClear = React.useCallback(() => {
|
|
588
584
|
setFilterValues({})
|
|
@@ -703,11 +699,15 @@ export default function CustomersDealsPage() {
|
|
|
703
699
|
if (selectedPersonIds.length) selectedPersonIds.forEach((id) => params.append('personId', id))
|
|
704
700
|
if (selectedCompanyIds.length) selectedCompanyIds.forEach((id) => params.append('companyId', id))
|
|
705
701
|
if (page > 1) params.set('page', String(page))
|
|
702
|
+
const advancedParams = serializeAdvancedFilter(advancedFilterState)
|
|
703
|
+
for (const [key, val] of Object.entries(advancedParams)) {
|
|
704
|
+
params.set(key, val)
|
|
705
|
+
}
|
|
706
706
|
const next = params.toString()
|
|
707
707
|
if (queryRef.current === next) return
|
|
708
708
|
queryRef.current = next
|
|
709
709
|
router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false })
|
|
710
|
-
}, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds])
|
|
710
|
+
}, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, advancedFilterState])
|
|
711
711
|
|
|
712
712
|
const handleRefresh = React.useCallback(() => {
|
|
713
713
|
peopleCacheRef.current.clear()
|
|
@@ -806,6 +806,8 @@ export default function CustomersDealsPage() {
|
|
|
806
806
|
const personOptions = peopleState.options
|
|
807
807
|
const companyOptions = companiesState.options
|
|
808
808
|
|
|
809
|
+
const peopleIdToLabel = peopleState.idToLabel
|
|
810
|
+
const companyIdToLabel = companiesState.idToLabel
|
|
809
811
|
const filters = React.useMemo<FilterDef[]>(() => [
|
|
810
812
|
{
|
|
811
813
|
id: 'people',
|
|
@@ -814,6 +816,7 @@ export default function CustomersDealsPage() {
|
|
|
814
816
|
options: personOptions,
|
|
815
817
|
loadOptions: loadPeopleOptions,
|
|
816
818
|
placeholder: t('customers.deals.list.filters.peoplePlaceholder'),
|
|
819
|
+
formatValue: (value: string) => peopleIdToLabel[value] ?? value,
|
|
817
820
|
},
|
|
818
821
|
{
|
|
819
822
|
id: 'companies',
|
|
@@ -822,8 +825,9 @@ export default function CustomersDealsPage() {
|
|
|
822
825
|
options: companyOptions,
|
|
823
826
|
loadOptions: loadCompanyOptions,
|
|
824
827
|
placeholder: t('customers.deals.list.filters.companiesPlaceholder'),
|
|
828
|
+
formatValue: (value: string) => companyIdToLabel[value] ?? value,
|
|
825
829
|
},
|
|
826
|
-
], [companyOptions, loadCompanyOptions, loadPeopleOptions, personOptions, t])
|
|
830
|
+
], [companyIdToLabel, companyOptions, loadCompanyOptions, loadPeopleOptions, peopleIdToLabel, personOptions, t])
|
|
827
831
|
|
|
828
832
|
const { data: customFieldDefs = [] } = useCustomFieldDefs([E.customers.customer_deal], {
|
|
829
833
|
keyExtras: [scopeVersion, reloadToken],
|
|
@@ -133,6 +133,8 @@ const createCommentCommand: CommandHandler<CommentCreateInput, { commentId: stri
|
|
|
133
133
|
tenantId: snapshot?.tenantId ?? null,
|
|
134
134
|
organizationId: snapshot?.organizationId ?? null,
|
|
135
135
|
snapshotAfter: snapshot ?? null,
|
|
136
|
+
relatedResourceKind: snapshot?.dealId ? 'customers.deal' : null,
|
|
137
|
+
relatedResourceId: snapshot?.dealId ?? null,
|
|
136
138
|
payload: {
|
|
137
139
|
undo: {
|
|
138
140
|
after: snapshot ?? null,
|
|
@@ -226,6 +228,8 @@ const updateCommentCommand: CommandHandler<CommentUpdateInput, { commentId: stri
|
|
|
226
228
|
organizationId: before.organizationId,
|
|
227
229
|
snapshotBefore: before,
|
|
228
230
|
snapshotAfter: afterSnapshot ?? null,
|
|
231
|
+
relatedResourceKind: (afterSnapshot?.dealId ?? before.dealId) ? 'customers.deal' : null,
|
|
232
|
+
relatedResourceId: afterSnapshot?.dealId ?? before.dealId ?? null,
|
|
229
233
|
changes,
|
|
230
234
|
payload: {
|
|
231
235
|
undo: {
|
|
@@ -332,6 +336,8 @@ const deleteCommentCommand: CommandHandler<{ body?: Record<string, unknown>; que
|
|
|
332
336
|
tenantId: before.tenantId,
|
|
333
337
|
organizationId: before.organizationId,
|
|
334
338
|
snapshotBefore: before,
|
|
339
|
+
relatedResourceKind: before.dealId ? 'customers.deal' : null,
|
|
340
|
+
relatedResourceId: before.dealId ?? null,
|
|
335
341
|
payload: {
|
|
336
342
|
undo: {
|
|
337
343
|
before,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { Search, Check, Settings2 } from 'lucide-react'
|
|
5
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
7
8
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
@@ -18,6 +19,8 @@ import type { RoleAssignment } from './RoleAssignmentRow'
|
|
|
18
19
|
import { fetchAssignableStaffMembersPage } from './assignableStaff'
|
|
19
20
|
import { getInitials } from './utils'
|
|
20
21
|
|
|
22
|
+
const MANAGE_ROLE_TYPES_HREF = '/backend/config/customers'
|
|
23
|
+
|
|
21
24
|
type StaffMember = {
|
|
22
25
|
id: string
|
|
23
26
|
displayName: string
|
|
@@ -34,6 +37,7 @@ interface AssignRoleDialogProps {
|
|
|
34
37
|
existingRoleTypes?: Set<string>
|
|
35
38
|
existingAssignments?: RoleAssignment[]
|
|
36
39
|
initialRoleType?: string | null
|
|
40
|
+
canManageRoleTypes?: boolean
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
type StepId = 1 | 2 | 3
|
|
@@ -55,6 +59,7 @@ export function AssignRoleDialog({
|
|
|
55
59
|
existingRoleTypes,
|
|
56
60
|
existingAssignments = [],
|
|
57
61
|
initialRoleType = null,
|
|
62
|
+
canManageRoleTypes = false,
|
|
58
63
|
}: AssignRoleDialogProps) {
|
|
59
64
|
const t = useT()
|
|
60
65
|
const [step, setStep] = React.useState<StepId>(1)
|
|
@@ -401,6 +406,16 @@ export function AssignRoleDialog({
|
|
|
401
406
|
</Badge>
|
|
402
407
|
) : null}
|
|
403
408
|
</div>
|
|
409
|
+
{canManageRoleTypes ? (
|
|
410
|
+
<Link
|
|
411
|
+
href={MANAGE_ROLE_TYPES_HREF}
|
|
412
|
+
className="mt-3 inline-flex items-center gap-1.5 text-xs font-medium text-primary hover:underline"
|
|
413
|
+
data-testid="assign-role-dialog-manage-role-types"
|
|
414
|
+
>
|
|
415
|
+
<Settings2 className="size-3.5" aria-hidden="true" />
|
|
416
|
+
{t('customers.roles.dialog.manageRoleTypes', 'Manage role types')}
|
|
417
|
+
</Link>
|
|
418
|
+
) : null}
|
|
404
419
|
</div>
|
|
405
420
|
|
|
406
421
|
{!availableRoleTypes.length ? (
|
|
@@ -434,14 +449,27 @@ export function AssignRoleDialog({
|
|
|
434
449
|
</Badge>
|
|
435
450
|
</div>
|
|
436
451
|
</div>
|
|
437
|
-
<
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
452
|
+
<div className="flex items-center gap-2">
|
|
453
|
+
{canManageRoleTypes ? (
|
|
454
|
+
<Button asChild variant="ghost" size="sm">
|
|
455
|
+
<Link
|
|
456
|
+
href={MANAGE_ROLE_TYPES_HREF}
|
|
457
|
+
data-testid="assign-role-dialog-manage-role-types-step2"
|
|
458
|
+
>
|
|
459
|
+
<Settings2 className="mr-1 size-3.5" aria-hidden="true" />
|
|
460
|
+
{t('customers.roles.dialog.manageRoleTypes', 'Manage role types')}
|
|
461
|
+
</Link>
|
|
462
|
+
</Button>
|
|
463
|
+
) : null}
|
|
464
|
+
<Button
|
|
465
|
+
type="button"
|
|
466
|
+
variant="outline"
|
|
467
|
+
size="sm"
|
|
468
|
+
onClick={() => setStep(1)}
|
|
469
|
+
>
|
|
470
|
+
{t('customers.roles.dialog.change', 'Change')}
|
|
471
|
+
</Button>
|
|
472
|
+
</div>
|
|
445
473
|
</div>
|
|
446
474
|
</div>
|
|
447
475
|
|
|
@@ -7,7 +7,9 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
9
9
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
10
|
+
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
10
11
|
import { CompanyTagsDialog } from './CompanyTagsDialog'
|
|
12
|
+
import { ObjectHistoryButton } from './ObjectHistoryButton'
|
|
11
13
|
import { invalidateCustomerDictionary, useCustomerDictionary } from './hooks/useCustomerDictionary'
|
|
12
14
|
import { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'
|
|
13
15
|
import type { TagSummary } from './types'
|
|
@@ -16,6 +18,8 @@ import type { CompanyOverview } from '../formConfig'
|
|
|
16
18
|
import type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'
|
|
17
19
|
import { formatFallbackLabel } from './utils'
|
|
18
20
|
|
|
21
|
+
const HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'
|
|
22
|
+
|
|
19
23
|
type CompanyDetailHeaderProps = {
|
|
20
24
|
data: CompanyOverview
|
|
21
25
|
onTagsChange: (tags: TagSummary[]) => void
|
|
@@ -191,6 +195,27 @@ export function CompanyDetailHeader({
|
|
|
191
195
|
{/* Right side: actions */}
|
|
192
196
|
<div className="flex w-full shrink-0 flex-col items-start gap-3 sm:w-auto sm:items-end">
|
|
193
197
|
<div className="flex w-full flex-wrap items-center justify-start gap-2 sm:w-auto sm:justify-end">
|
|
198
|
+
<SendObjectMessageDialog
|
|
199
|
+
object={{
|
|
200
|
+
entityModule: 'customers',
|
|
201
|
+
entityType: 'company',
|
|
202
|
+
entityId: company.id,
|
|
203
|
+
previewData: {
|
|
204
|
+
title: displayName,
|
|
205
|
+
subtitle: company.primaryEmail ?? profile?.websiteUrl ?? undefined,
|
|
206
|
+
},
|
|
207
|
+
}}
|
|
208
|
+
viewHref={`/backend/customers/companies-v2/${company.id}`}
|
|
209
|
+
buttonVariant="outline"
|
|
210
|
+
buttonSize="icon"
|
|
211
|
+
buttonClassName={HEADER_ICON_BUTTON_CLASS}
|
|
212
|
+
buttonLabel={t('customers.companies.detail.actions.sendMessage', 'Send message')}
|
|
213
|
+
/>
|
|
214
|
+
<ObjectHistoryButton
|
|
215
|
+
resourceKind="customers.company"
|
|
216
|
+
resourceId={company.id}
|
|
217
|
+
organizationId={company.organizationId ?? undefined}
|
|
218
|
+
/>
|
|
194
219
|
<IconButton
|
|
195
220
|
variant="outline"
|
|
196
221
|
size="sm"
|
|
@@ -6,7 +6,9 @@ import { cn } from '@open-mercato/shared/lib/utils'
|
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
8
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
9
|
+
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
9
10
|
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
11
|
+
import { ObjectHistoryButton } from './ObjectHistoryButton'
|
|
10
12
|
import { useCustomerDictionary } from './hooks/useCustomerDictionary'
|
|
11
13
|
import { formatFallbackLabel } from './utils'
|
|
12
14
|
import { isTerminalPipelineOutcomeLabel } from './pipelineStageUtils'
|
|
@@ -74,6 +76,8 @@ const headerChipDotVariantClasses: Record<HeaderChipVariant, string> = {
|
|
|
74
76
|
neutral: 'bg-status-neutral-icon',
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
const HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'
|
|
80
|
+
|
|
77
81
|
function HeaderChip({
|
|
78
82
|
children,
|
|
79
83
|
icon,
|
|
@@ -227,6 +231,9 @@ export function DealDetailHeader({
|
|
|
227
231
|
const showStatusChip = statusLabel && (!deal.closureOutcome || !isTerminalPipelineOutcomeLabel(deal.status))
|
|
228
232
|
const pipelineBadgeLabel = pipelineName ?? null
|
|
229
233
|
const canMoveStage = stageOptions.length > 0 && !deal.closureOutcome && typeof onStageChange === 'function'
|
|
234
|
+
const messageSubtitle = React.useMemo(() => (
|
|
235
|
+
[companyLabel, amountLabel].filter(Boolean).join(' · ') || statusLabel || undefined
|
|
236
|
+
), [amountLabel, companyLabel, statusLabel])
|
|
230
237
|
|
|
231
238
|
return (
|
|
232
239
|
<div className="flex flex-col gap-5 xl:flex-row xl:items-start xl:justify-between">
|
|
@@ -307,6 +314,28 @@ export function DealDetailHeader({
|
|
|
307
314
|
isSaving={isStageSaving}
|
|
308
315
|
/>
|
|
309
316
|
) : null}
|
|
317
|
+
<SendObjectMessageDialog
|
|
318
|
+
object={{
|
|
319
|
+
entityModule: 'customers',
|
|
320
|
+
entityType: 'deal',
|
|
321
|
+
entityId: deal.id,
|
|
322
|
+
previewData: {
|
|
323
|
+
title: deal.title || t('customers.deals.detail.untitled', 'Untitled deal'),
|
|
324
|
+
subtitle: messageSubtitle,
|
|
325
|
+
},
|
|
326
|
+
}}
|
|
327
|
+
viewHref={`/backend/customers/deals/${deal.id}`}
|
|
328
|
+
buttonVariant="outline"
|
|
329
|
+
buttonSize="icon"
|
|
330
|
+
buttonClassName={HEADER_ICON_BUTTON_CLASS}
|
|
331
|
+
buttonLabel={t('customers.deals.detail.actions.sendMessage', 'Send message')}
|
|
332
|
+
/>
|
|
333
|
+
<ObjectHistoryButton
|
|
334
|
+
resourceKind="customers.deal"
|
|
335
|
+
resourceId={deal.id}
|
|
336
|
+
organizationId={deal.organizationId ?? undefined}
|
|
337
|
+
includeRelated
|
|
338
|
+
/>
|
|
310
339
|
<IconButton
|
|
311
340
|
variant="outline"
|
|
312
341
|
size="sm"
|
|
@@ -85,11 +85,11 @@ export function DealWonPopup({
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
|
|
88
|
-
<DialogContent className="overflow-hidden p-0 sm:max-w-[420px]">
|
|
88
|
+
<DialogContent className="max-h-[90vh] overflow-hidden p-0 sm:max-w-[420px]">
|
|
89
89
|
<VisuallyHidden>
|
|
90
90
|
<DialogTitle>{t('customers.deals.detail.won.title', 'Closed successfully')}</DialogTitle>
|
|
91
91
|
</VisuallyHidden>
|
|
92
|
-
<div className="overflow-
|
|
92
|
+
<div className="max-h-[90vh] overflow-y-auto rounded-2xl bg-card">
|
|
93
93
|
<div className="px-6 pb-5 pt-6">
|
|
94
94
|
{/* TODO(ds-review): decorative gradient — consider defining a named gradient token if reused */}
|
|
95
95
|
<div className="flex h-[200px] items-center justify-center rounded-2xl bg-[linear-gradient(135deg,rgba(141,150,244,0.5),rgba(198,203,254,0.95))] text-foreground">
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { SquarePen, Calendar, Check } from 'lucide-react'
|
|
4
|
+
import { SquarePen, Calendar, Check, ChevronDown, ChevronUp } from 'lucide-react'
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
7
|
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
8
8
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
|
+
import { usePersistedBooleanFlag } from '@open-mercato/ui/backend/crud/usePersistedBooleanFlag'
|
|
10
11
|
import { ActivityTypeSelector, type ActivityType } from './ActivityTypeSelector'
|
|
11
12
|
import { MiniWeekCalendar } from './MiniWeekCalendar'
|
|
12
13
|
|
|
@@ -66,6 +67,24 @@ export function InlineActivityComposer({
|
|
|
66
67
|
const [errors, setErrors] = React.useState<Record<string, string>>({})
|
|
67
68
|
const descriptionRef = React.useRef<HTMLTextAreaElement>(null)
|
|
68
69
|
|
|
70
|
+
const weekPreviewStorageKey = `om:inline-composer:week-preview:${entityType}`
|
|
71
|
+
const { value: weekPreviewHidden, toggle: toggleWeekPreview } = usePersistedBooleanFlag(
|
|
72
|
+
weekPreviewStorageKey,
|
|
73
|
+
false,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const resizeDescription = React.useCallback(() => {
|
|
77
|
+
const el = descriptionRef.current
|
|
78
|
+
if (!el) return
|
|
79
|
+
el.style.height = 'auto'
|
|
80
|
+
const maxHeight = 200
|
|
81
|
+
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
React.useEffect(() => {
|
|
85
|
+
resizeDescription()
|
|
86
|
+
}, [description, resizeDescription])
|
|
87
|
+
|
|
69
88
|
const handleTypeSelect = React.useCallback((type: ActivityType) => {
|
|
70
89
|
setSelectedType((previous) => (previous === type ? null : type))
|
|
71
90
|
setErrors({})
|
|
@@ -191,19 +210,26 @@ export function InlineActivityComposer({
|
|
|
191
210
|
{/* Description + date row */}
|
|
192
211
|
<div className="mt-4 flex items-start gap-3">
|
|
193
212
|
<div className="flex-1">
|
|
213
|
+
<label
|
|
214
|
+
htmlFor="inline-activity-composer-description"
|
|
215
|
+
className="mb-1 block text-xs font-medium text-muted-foreground"
|
|
216
|
+
>
|
|
217
|
+
{t('customers.activityComposer.descriptionLabel', 'Description')}
|
|
218
|
+
</label>
|
|
194
219
|
<textarea
|
|
195
220
|
ref={descriptionRef}
|
|
221
|
+
id="inline-activity-composer-description"
|
|
196
222
|
value={description}
|
|
197
223
|
onChange={(event) => setDescription(event.target.value)}
|
|
198
224
|
placeholder={t('customers.activityComposer.descriptionPlaceholder', 'What happened?')}
|
|
199
|
-
className="min-h-[
|
|
200
|
-
rows={
|
|
225
|
+
className="min-h-[72px] w-full resize-none rounded-lg border bg-background px-3 py-2.5 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
|
226
|
+
rows={3}
|
|
201
227
|
/>
|
|
202
228
|
{errors.description ? (
|
|
203
229
|
<p className="mt-1 text-xs text-destructive">{errors.description}</p>
|
|
204
230
|
) : null}
|
|
205
231
|
</div>
|
|
206
|
-
<label className="flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground">
|
|
232
|
+
<label className="mt-[22px] flex shrink-0 cursor-pointer items-center gap-2 rounded-lg border bg-background px-3 py-2.5 text-sm text-muted-foreground">
|
|
207
233
|
<Calendar className="size-4" />
|
|
208
234
|
<span className="text-sm font-medium text-foreground">{formatDateBadge(occurredAt, t)}</span>
|
|
209
235
|
<input
|
|
@@ -215,9 +241,42 @@ export function InlineActivityComposer({
|
|
|
215
241
|
</label>
|
|
216
242
|
</div>
|
|
217
243
|
|
|
218
|
-
{/* Mini calendar preview */}
|
|
219
244
|
<div className="mt-4">
|
|
220
|
-
<
|
|
245
|
+
<div className="mb-2 flex items-center justify-between">
|
|
246
|
+
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
247
|
+
{t('customers.activityComposer.weekPreviewTitle', 'This week')}
|
|
248
|
+
</span>
|
|
249
|
+
<Button
|
|
250
|
+
type="button"
|
|
251
|
+
variant="ghost"
|
|
252
|
+
size="sm"
|
|
253
|
+
onClick={toggleWeekPreview}
|
|
254
|
+
aria-expanded={!weekPreviewHidden}
|
|
255
|
+
aria-controls="inline-activity-composer-week-preview"
|
|
256
|
+
className="h-auto px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
|
|
257
|
+
>
|
|
258
|
+
{weekPreviewHidden ? (
|
|
259
|
+
<>
|
|
260
|
+
<ChevronDown className="mr-1 size-3" />
|
|
261
|
+
{t('customers.activityComposer.showWeekPreview', 'Show week preview')}
|
|
262
|
+
</>
|
|
263
|
+
) : (
|
|
264
|
+
<>
|
|
265
|
+
<ChevronUp className="mr-1 size-3" />
|
|
266
|
+
{t('customers.activityComposer.hideWeekPreview', 'Hide week preview')}
|
|
267
|
+
</>
|
|
268
|
+
)}
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
{!weekPreviewHidden ? (
|
|
272
|
+
<div id="inline-activity-composer-week-preview">
|
|
273
|
+
<MiniWeekCalendar
|
|
274
|
+
entityId={entityId}
|
|
275
|
+
useCanonicalInteractions={useCanonicalInteractions}
|
|
276
|
+
refreshRef={calendarRefreshRef}
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
) : null}
|
|
221
280
|
</div>
|
|
222
281
|
|
|
223
282
|
{/* Scheduled for (optional) */}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'
|
|
5
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
+
import type { VersionHistoryConfig } from '@open-mercato/ui/backend/version-history'
|
|
7
|
+
|
|
8
|
+
export type ObjectHistoryButtonProps = {
|
|
9
|
+
resourceKind: VersionHistoryConfig['resourceKind']
|
|
10
|
+
resourceId: VersionHistoryConfig['resourceId']
|
|
11
|
+
resourceIdFallback?: VersionHistoryConfig['resourceIdFallback']
|
|
12
|
+
organizationId?: VersionHistoryConfig['organizationId']
|
|
13
|
+
includeRelated?: VersionHistoryConfig['includeRelated']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const OUTLINE_ICON_BUTTON_CLASSES =
|
|
17
|
+
'size-8 rounded-md border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50'
|
|
18
|
+
|
|
19
|
+
export function ObjectHistoryButton({
|
|
20
|
+
resourceKind,
|
|
21
|
+
resourceId,
|
|
22
|
+
resourceIdFallback,
|
|
23
|
+
organizationId,
|
|
24
|
+
includeRelated,
|
|
25
|
+
}: ObjectHistoryButtonProps) {
|
|
26
|
+
const t = useT()
|
|
27
|
+
const config = React.useMemo<VersionHistoryConfig>(
|
|
28
|
+
() => ({
|
|
29
|
+
resourceKind,
|
|
30
|
+
resourceId,
|
|
31
|
+
resourceIdFallback,
|
|
32
|
+
organizationId,
|
|
33
|
+
includeRelated,
|
|
34
|
+
}),
|
|
35
|
+
[resourceKind, resourceId, resourceIdFallback, organizationId, includeRelated],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<VersionHistoryAction
|
|
40
|
+
config={config}
|
|
41
|
+
t={t}
|
|
42
|
+
buttonClassName={OUTLINE_ICON_BUTTON_CLASSES}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default ObjectHistoryButton
|
|
@@ -8,7 +8,9 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
8
8
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
9
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
10
10
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
11
|
+
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
11
12
|
import { useQueryClient } from '@tanstack/react-query'
|
|
13
|
+
import { ObjectHistoryButton } from './ObjectHistoryButton'
|
|
12
14
|
import { PersonTagsDialog } from './PersonTagsDialog'
|
|
13
15
|
import { useCustomerDictionary, invalidateCustomerDictionary } from './hooks/useCustomerDictionary'
|
|
14
16
|
import { renderDictionaryIcon } from '../../../dictionaries/components/dictionaryAppearance'
|
|
@@ -18,6 +20,8 @@ import type { PersonOverview } from '../formConfig'
|
|
|
18
20
|
import type { CustomerDictionaryMap } from '@open-mercato/core/modules/customers/lib/dictionaries'
|
|
19
21
|
import { getInitials, formatFallbackLabel } from './utils'
|
|
20
22
|
|
|
23
|
+
const HEADER_ICON_BUTTON_CLASS = 'size-8 rounded-md'
|
|
24
|
+
|
|
21
25
|
type PersonDetailHeaderProps = {
|
|
22
26
|
data: PersonOverview
|
|
23
27
|
onTagsChange: (tags: TagSummary[]) => void
|
|
@@ -243,6 +247,27 @@ export function PersonDetailHeader({
|
|
|
243
247
|
|
|
244
248
|
{/* Right side: actions */}
|
|
245
249
|
<div className="flex w-full shrink-0 items-center justify-start gap-2 sm:w-auto sm:justify-end">
|
|
250
|
+
<SendObjectMessageDialog
|
|
251
|
+
object={{
|
|
252
|
+
entityModule: 'customers',
|
|
253
|
+
entityType: 'person',
|
|
254
|
+
entityId: person.id,
|
|
255
|
+
previewData: {
|
|
256
|
+
title: displayName,
|
|
257
|
+
subtitle: person.primaryEmail ?? companyName ?? undefined,
|
|
258
|
+
},
|
|
259
|
+
}}
|
|
260
|
+
viewHref={`/backend/customers/people-v2/${person.id}`}
|
|
261
|
+
buttonVariant="outline"
|
|
262
|
+
buttonSize="icon"
|
|
263
|
+
buttonClassName={HEADER_ICON_BUTTON_CLASS}
|
|
264
|
+
buttonLabel={t('customers.people.detail.actions.sendMessage', 'Send message')}
|
|
265
|
+
/>
|
|
266
|
+
<ObjectHistoryButton
|
|
267
|
+
resourceKind="customers.person"
|
|
268
|
+
resourceId={person.id}
|
|
269
|
+
organizationId={person.organizationId ?? undefined}
|
|
270
|
+
/>
|
|
246
271
|
<IconButton
|
|
247
272
|
variant="outline"
|
|
248
273
|
size="sm"
|