@open-mercato/core 0.6.5-develop.5382.1.f542de69af → 0.6.5
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/bootstrap.js +46 -6
- package/dist/bootstrap.js.map +2 -2
- package/dist/generated/entities/organization/index.js +2 -0
- package/dist/generated/entities/organization/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/crmFixtures.js +4 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/attachments/api/route.js +2 -0
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/lib/access.js +18 -0
- package/dist/modules/attachments/lib/access.js.map +2 -2
- package/dist/modules/audit_logs/data/entities.js +2 -1
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js +13 -0
- package/dist/modules/audit_logs/migrations/Migration20260611104500.js.map +7 -0
- package/dist/modules/audit_logs/services/accessLogService.js +10 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
- package/dist/modules/auth/api/admin/nav.js +9 -0
- package/dist/modules/auth/api/admin/nav.js.map +2 -2
- package/dist/modules/auth/api/login.js +4 -13
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/data/entities.js +3 -1
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/backendChrome.js +35 -2
- package/dist/modules/auth/lib/backendChrome.js.map +2 -2
- package/dist/modules/auth/lib/consentIntegrity.js +3 -3
- package/dist/modules/auth/lib/consentIntegrity.js.map +2 -2
- package/dist/modules/auth/migrations/Migration20260611103000.js +15 -0
- package/dist/modules/auth/migrations/Migration20260611103000.js.map +7 -0
- package/dist/modules/auth/services/authService.js +5 -3
- package/dist/modules/auth/services/authService.js.map +2 -2
- package/dist/modules/auth/services/rbacService.js +3 -2
- package/dist/modules/auth/services/rbacService.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js +0 -3
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +43 -2
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/deals/summary/route.js +402 -0
- package/dist/modules/customers/api/deals/summary/route.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js +16 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js +22 -5
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealData.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +12 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +221 -56
- package/dist/modules/customers/backend/customers/deals/page.js.map +3 -3
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +18 -0
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/cli.js +15 -9
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/components/DealsKpiStrip.js +282 -0
- package/dist/modules/customers/components/DealsKpiStrip.js.map +7 -0
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js +0 -1
- package/dist/modules/customers/components/detail/ConfirmDealLostDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +100 -17
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +11 -3
- package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +1 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/kpi/PipelineStageBar.js +63 -0
- package/dist/modules/customers/components/kpi/PipelineStageBar.js.map +7 -0
- package/dist/modules/customers/lib/dealsMetrics.js +82 -0
- package/dist/modules/customers/lib/dealsMetrics.js.map +7 -0
- package/dist/modules/directory/api/organization-branding/route.js +214 -0
- package/dist/modules/directory/api/organization-branding/route.js.map +7 -0
- package/dist/modules/directory/api/organizations/route.js +7 -0
- package/dist/modules/directory/api/organizations/route.js.map +3 -3
- package/dist/modules/directory/backend/directory/branding/page.js +214 -0
- package/dist/modules/directory/backend/directory/branding/page.js.map +7 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js +26 -0
- package/dist/modules/directory/backend/directory/branding/page.meta.js.map +7 -0
- package/dist/modules/directory/commands/organizations.js +8 -1
- package/dist/modules/directory/commands/organizations.js.map +2 -2
- package/dist/modules/directory/data/entities.js +3 -0
- package/dist/modules/directory/data/entities.js.map +2 -2
- package/dist/modules/directory/data/validators.js +9 -0
- package/dist/modules/directory/data/validators.js.map +2 -2
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js +13 -0
- package/dist/modules/directory/migrations/Migration20260607222259_directory.js.map +7 -0
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +2 -1
- package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +2 -2
- package/dist/modules/directory/utils/organizationScope.js +59 -27
- package/dist/modules/directory/utils/organizationScope.js.map +2 -2
- package/dist/modules/entities/api/definitions.batch.js +2 -1
- package/dist/modules/entities/api/definitions.batch.js.map +2 -2
- package/dist/modules/entities/api/entities.js +7 -0
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +26 -15
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js +14 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/create/page.js.map +2 -2
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js +12 -0
- package/dist/modules/entities/backend/entities/user/[entityId]/records/page.js.map +2 -2
- package/dist/modules/entities/components/useRecordsEntityGuard.js +30 -0
- package/dist/modules/entities/components/useRecordsEntityGuard.js.map +7 -0
- package/dist/modules/query_index/data/entities.js +2 -1
- package/dist/modules/query_index/data/entities.js.map +2 -2
- package/dist/modules/query_index/lib/engine.js +4 -2
- package/dist/modules/query_index/lib/engine.js.map +2 -2
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js +16 -0
- package/dist/modules/query_index/migrations/Migration20260611103000_query_index.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +7 -5
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js +2 -1
- package/dist/modules/sales/components/documents/SalesDocumentsTable.js.map +2 -2
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js +10 -0
- package/dist/modules/sales/components/documents/salesDocumentsColumns.js.map +7 -0
- package/dist/modules/staff/api/team-members.js +9 -2
- package/dist/modules/staff/api/team-members.js.map +2 -2
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js +24 -1
- package/dist/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.js.map +2 -2
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js +11 -6
- package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
- package/dist/modules/staff/commands/team-members.js +1 -1
- package/dist/modules/staff/commands/team-members.js.map +2 -2
- package/dist/modules/staff/components/TeamMemberForm.js +1 -1
- package/dist/modules/staff/components/TeamMemberForm.js.map +2 -2
- package/dist/modules/staff/lib/scheduleSwitch.js +23 -0
- package/dist/modules/staff/lib/scheduleSwitch.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/create/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +1 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +1 -2
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialog.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +4 -13
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +1 -4
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -5
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
- package/generated/entities/organization/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +11 -12
- package/src/bootstrap.ts +65 -7
- package/src/helpers/integration/crmFixtures.ts +21 -1
- package/src/modules/attachments/AGENTS.md +79 -0
- package/src/modules/attachments/api/route.ts +2 -0
- package/src/modules/attachments/lib/access.ts +36 -0
- package/src/modules/audit_logs/data/entities.ts +1 -0
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/audit_logs/migrations/Migration20260611104500.ts +13 -0
- package/src/modules/audit_logs/services/accessLogService.ts +15 -0
- package/src/modules/auth/api/admin/nav.ts +9 -0
- package/src/modules/auth/api/login.ts +13 -13
- package/src/modules/auth/data/entities.ts +2 -0
- package/src/modules/auth/i18n/de.json +0 -1
- package/src/modules/auth/i18n/en.json +0 -1
- package/src/modules/auth/i18n/es.json +0 -1
- package/src/modules/auth/i18n/pl.json +0 -1
- package/src/modules/auth/lib/backendChrome.tsx +37 -1
- package/src/modules/auth/lib/consentIntegrity.ts +6 -3
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/auth/migrations/Migration20260611103000.ts +21 -0
- package/src/modules/auth/services/authService.ts +24 -4
- package/src/modules/auth/services/rbacService.ts +11 -2
- package/src/modules/customer_accounts/backend/customer_accounts/settings/domain/components/Diagnostics.tsx +0 -3
- package/src/modules/customers/api/deals/route.ts +51 -2
- package/src/modules/customers/api/deals/summary/route.ts +496 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealActivities.ts +28 -6
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealData.ts +33 -6
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +17 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +254 -66
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +1 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +18 -0
- package/src/modules/customers/cli.ts +15 -15
- package/src/modules/customers/components/DealsKpiStrip.tsx +389 -0
- package/src/modules/customers/components/detail/ConfirmDealLostDialog.tsx +0 -1
- package/src/modules/customers/components/detail/DealForm.tsx +121 -19
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +12 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +1 -2
- package/src/modules/customers/components/kpi/PipelineStageBar.tsx +77 -0
- package/src/modules/customers/i18n/de.json +43 -0
- package/src/modules/customers/i18n/en.json +43 -0
- package/src/modules/customers/i18n/es.json +43 -0
- package/src/modules/customers/i18n/pl.json +43 -0
- package/src/modules/customers/lib/dealsMetrics.ts +159 -0
- package/src/modules/directory/api/organization-branding/route.ts +238 -0
- package/src/modules/directory/api/organizations/route.ts +7 -0
- package/src/modules/directory/backend/directory/branding/page.meta.ts +24 -0
- package/src/modules/directory/backend/directory/branding/page.tsx +248 -0
- package/src/modules/directory/commands/organizations.ts +9 -1
- package/src/modules/directory/data/entities.ts +3 -0
- package/src/modules/directory/data/validators.ts +12 -0
- package/src/modules/directory/i18n/de.json +21 -0
- package/src/modules/directory/i18n/en.json +21 -0
- package/src/modules/directory/i18n/es.json +21 -0
- package/src/modules/directory/i18n/pl.json +21 -0
- package/src/modules/directory/migrations/.snapshot-open-mercato.json +40 -0
- package/src/modules/directory/migrations/Migration20260607222259_directory.ts +13 -0
- package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +3 -1
- package/src/modules/directory/utils/organizationScope.ts +85 -30
- package/src/modules/entities/api/definitions.batch.ts +11 -7
- package/src/modules/entities/api/entities.ts +11 -0
- package/src/modules/entities/api/records.ts +46 -25
- package/src/modules/entities/backend/entities/user/[entityId]/records/[recordId]/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/create/page.tsx +15 -0
- package/src/modules/entities/backend/entities/user/[entityId]/records/page.tsx +23 -0
- package/src/modules/entities/components/useRecordsEntityGuard.ts +41 -0
- package/src/modules/entities/i18n/de.json +1 -0
- package/src/modules/entities/i18n/en.json +1 -0
- package/src/modules/entities/i18n/es.json +1 -0
- package/src/modules/entities/i18n/pl.json +1 -0
- package/src/modules/query_index/data/entities.ts +1 -0
- package/src/modules/query_index/lib/engine.ts +11 -5
- package/src/modules/query_index/migrations/.snapshot-open-mercato.json +11 -0
- package/src/modules/query_index/migrations/Migration20260611103000_query_index.ts +29 -0
- package/src/modules/sales/commands/documents.ts +7 -5
- package/src/modules/sales/components/documents/SalesDocumentsTable.tsx +2 -1
- package/src/modules/sales/components/documents/salesDocumentsColumns.ts +6 -0
- package/src/modules/staff/api/team-members.ts +9 -2
- package/src/modules/staff/api/timesheets/time-entries/[id]/timer-start/route.ts +31 -1
- package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +18 -8
- package/src/modules/staff/commands/team-members.ts +5 -2
- package/src/modules/staff/components/TeamMemberForm.tsx +4 -1
- package/src/modules/staff/i18n/de.json +1 -0
- package/src/modules/staff/i18n/en.json +1 -0
- package/src/modules/staff/i18n/es.json +1 -0
- package/src/modules/staff/i18n/pl.json +1 -0
- package/src/modules/staff/lib/scheduleSwitch.ts +46 -0
- package/src/modules/workflows/backend/definitions/create/page.tsx +1 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +1 -2
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +1 -2
- package/src/modules/workflows/components/NodeEditDialog.tsx +1 -4
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +4 -7
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +1 -2
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -3
|
@@ -3,13 +3,40 @@ import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
|
3
3
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
4
4
|
import type { DealDetailPayload } from './types'
|
|
5
5
|
|
|
6
|
+
type LoadDataOptions = {
|
|
7
|
+
cache?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
type UseDealDataResult = {
|
|
7
11
|
data: DealDetailPayload | null
|
|
8
12
|
setData: React.Dispatch<React.SetStateAction<DealDetailPayload | null>>
|
|
9
13
|
isLoading: boolean
|
|
10
14
|
error: string | null
|
|
11
15
|
isNotFound: boolean
|
|
12
|
-
loadData: () => Promise<void>
|
|
16
|
+
loadData: (options?: LoadDataOptions) => Promise<void>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DealDataCacheEntry = {
|
|
20
|
+
promise: Promise<DealDetailPayload>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const dealDataCache = new Map<string, DealDataCacheEntry>()
|
|
24
|
+
|
|
25
|
+
function fetchDealData(id: string, errorMessage: string, useCache: boolean): Promise<DealDetailPayload> {
|
|
26
|
+
const url = `/api/customers/deals/${encodeURIComponent(id)}?include=stages&view=lite`
|
|
27
|
+
const cached = dealDataCache.get(url)
|
|
28
|
+
if (useCache && cached) return cached.promise
|
|
29
|
+
const entry: DealDataCacheEntry = {
|
|
30
|
+
promise: readApiResultOrThrow<DealDetailPayload>(
|
|
31
|
+
url,
|
|
32
|
+
undefined,
|
|
33
|
+
{ errorMessage },
|
|
34
|
+
),
|
|
35
|
+
}
|
|
36
|
+
if (useCache) dealDataCache.set(url, entry)
|
|
37
|
+
return entry.promise.finally(() => {
|
|
38
|
+
if (dealDataCache.get(url) === entry) dealDataCache.delete(url)
|
|
39
|
+
})
|
|
13
40
|
}
|
|
14
41
|
|
|
15
42
|
export function useDealData(id: string): UseDealDataResult {
|
|
@@ -20,7 +47,7 @@ export function useDealData(id: string): UseDealDataResult {
|
|
|
20
47
|
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
21
48
|
const initialLoadDoneRef = React.useRef(false)
|
|
22
49
|
|
|
23
|
-
const loadData = React.useCallback(async () => {
|
|
50
|
+
const loadData = React.useCallback(async (options: LoadDataOptions = {}) => {
|
|
24
51
|
if (!id) {
|
|
25
52
|
setIsNotFound(true)
|
|
26
53
|
setIsLoading(false)
|
|
@@ -31,10 +58,10 @@ export function useDealData(id: string): UseDealDataResult {
|
|
|
31
58
|
}
|
|
32
59
|
setError(null)
|
|
33
60
|
try {
|
|
34
|
-
const payload = await
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
const payload = await fetchDealData(
|
|
62
|
+
id,
|
|
63
|
+
t('customers.deals.detail.error.load', 'Failed to load deal.'),
|
|
64
|
+
options.cache === true,
|
|
38
65
|
)
|
|
39
66
|
setData(payload)
|
|
40
67
|
} catch (loadError) {
|
|
@@ -105,7 +105,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
105
105
|
} = useDealActivities({ dealId: id, runMutationWithContext })
|
|
106
106
|
|
|
107
107
|
React.useEffect(() => {
|
|
108
|
-
void Promise.all([loadData(), loadPlannedActivities()])
|
|
108
|
+
void Promise.all([loadData({ cache: true }), loadPlannedActivities({ cache: true })])
|
|
109
109
|
}, [loadData, loadPlannedActivities])
|
|
110
110
|
|
|
111
111
|
const activityEntities = React.useMemo(
|
|
@@ -294,6 +294,20 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
294
294
|
})
|
|
295
295
|
}, [closeLostPopup, data, openScheduleEdit, selectedActivityEntity, t])
|
|
296
296
|
|
|
297
|
+
const currentPipelineName = data
|
|
298
|
+
? data.pipelineName ?? wonStats?.pipelineName ?? lostStats?.pipelineName ?? null
|
|
299
|
+
: wonStats?.pipelineName ?? lostStats?.pipelineName ?? null
|
|
300
|
+
const formPipelineOptions = React.useMemo(
|
|
301
|
+
() => data?.deal.pipelineId
|
|
302
|
+
? [{
|
|
303
|
+
id: data.deal.pipelineId,
|
|
304
|
+
name: currentPipelineName ?? t('customers.deals.detail.pipeline.defaultName', 'Current pipeline'),
|
|
305
|
+
isDefault: false,
|
|
306
|
+
}]
|
|
307
|
+
: [],
|
|
308
|
+
[currentPipelineName, data?.deal.pipelineId, t],
|
|
309
|
+
)
|
|
310
|
+
|
|
297
311
|
if (isLoading) {
|
|
298
312
|
return (
|
|
299
313
|
<Page>
|
|
@@ -338,7 +352,6 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
338
352
|
}
|
|
339
353
|
|
|
340
354
|
const amountLabel = formatCurrency(data.deal.valueAmount, data.deal.valueCurrency)
|
|
341
|
-
const currentPipelineName = data.pipelineName ?? wonStats?.pipelineName ?? lostStats?.pipelineName ?? null
|
|
342
355
|
const dealName = data.deal.title || t('customers.deals.detail.untitled', 'Untitled deal')
|
|
343
356
|
|
|
344
357
|
const zone1Content = (
|
|
@@ -353,6 +366,8 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
353
366
|
showVersionHistory={false}
|
|
354
367
|
showCancelAction={false}
|
|
355
368
|
onDirtyChange={setIsDirty}
|
|
369
|
+
initialPipelineOptions={formPipelineOptions}
|
|
370
|
+
initialPipelineStageOptions={data.pipelineStages}
|
|
356
371
|
collapsibleGroups={{ pageType: 'deal-detail-v3', chevronPosition: 'right' }}
|
|
357
372
|
sortableGroups={{ pageType: 'deal-detail-v3' }}
|
|
358
373
|
initialValues={{
|
|
@@ -21,13 +21,20 @@ import { coalesceLastOperations } from '@open-mercato/ui/backend/operations/stor
|
|
|
21
21
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
22
22
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
23
23
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
24
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
25
|
+
import { StatusBadge, type StatusBadgeVariant } from '@open-mercato/ui/primitives/status-badge'
|
|
26
|
+
import { Avatar, AvatarStack } from '@open-mercato/ui/primitives/avatar'
|
|
27
|
+
import { Tag } from '@open-mercato/ui/primitives/tag'
|
|
28
|
+
import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
|
|
29
|
+
import { Briefcase, AlertTriangle, X } from 'lucide-react'
|
|
30
|
+
import { formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
24
31
|
import { ViewTabsRow } from './pipeline/components/ViewTabsRow'
|
|
32
|
+
import { DealsKpiStrip } from '../../../components/DealsKpiStrip'
|
|
25
33
|
import { E } from '#generated/entities.ids.generated'
|
|
26
34
|
import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
|
|
27
35
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
28
36
|
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
29
37
|
import {
|
|
30
|
-
DictionaryValue,
|
|
31
38
|
type CustomerDictionaryKind,
|
|
32
39
|
type CustomerDictionaryMap,
|
|
33
40
|
} from '../../../lib/dictionaries'
|
|
@@ -149,20 +156,6 @@ function extractIdsFromParams(params: URLSearchParams | null | undefined, key: s
|
|
|
149
156
|
return normalizeIdCandidates(values)
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
function formatCurrency(amount: number | null | undefined, currency: string | null | undefined, fallback: string): string {
|
|
153
|
-
if (typeof amount !== 'number' || Number.isNaN(amount)) return fallback
|
|
154
|
-
try {
|
|
155
|
-
if (currency && currency.trim().length) {
|
|
156
|
-
const formatter = new Intl.NumberFormat(undefined, { style: 'currency', currency })
|
|
157
|
-
return formatter.format(amount)
|
|
158
|
-
}
|
|
159
|
-
const formatter = new Intl.NumberFormat(undefined, { style: 'decimal', maximumFractionDigits: 2 })
|
|
160
|
-
return formatter.format(amount)
|
|
161
|
-
} catch {
|
|
162
|
-
return currency ? `${amount} ${currency}` : String(amount)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
159
|
function formatDateValue(value: string | null | undefined, fallback: string): string {
|
|
167
160
|
if (!value) return fallback
|
|
168
161
|
const date = new Date(value)
|
|
@@ -170,6 +163,30 @@ function formatDateValue(value: string | null | undefined, fallback: string): st
|
|
|
170
163
|
return date.toLocaleDateString()
|
|
171
164
|
}
|
|
172
165
|
|
|
166
|
+
const STATUS_BADGE_VARIANTS: ReadonlySet<StatusBadgeVariant> = new Set([
|
|
167
|
+
'success',
|
|
168
|
+
'warning',
|
|
169
|
+
'error',
|
|
170
|
+
'info',
|
|
171
|
+
'neutral',
|
|
172
|
+
])
|
|
173
|
+
|
|
174
|
+
function coerceStatusBadgeVariant(
|
|
175
|
+
tone: ReturnType<typeof mapDictionaryColorToTone>,
|
|
176
|
+
): StatusBadgeVariant {
|
|
177
|
+
if (tone && STATUS_BADGE_VARIANTS.has(tone as StatusBadgeVariant)) {
|
|
178
|
+
return tone as StatusBadgeVariant
|
|
179
|
+
}
|
|
180
|
+
return 'neutral'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const groupedAmountFormatter = new Intl.NumberFormat(undefined, { maximumFractionDigits: 0 })
|
|
184
|
+
|
|
185
|
+
function formatGroupedAmount(amount: number | null | undefined): string | null {
|
|
186
|
+
if (typeof amount !== 'number' || Number.isNaN(amount)) return null
|
|
187
|
+
return groupedAmountFormatter.format(amount)
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
export default function CustomersDealsPage() {
|
|
174
191
|
const t = useT()
|
|
175
192
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
@@ -192,6 +209,7 @@ export default function CustomersDealsPage() {
|
|
|
192
209
|
const [isLoading, setIsLoading] = React.useState(false)
|
|
193
210
|
const [reloadToken, setReloadToken] = React.useState(0)
|
|
194
211
|
const [pendingDeleteId, setPendingDeleteId] = React.useState<string | null>(null)
|
|
212
|
+
const [needsAttentionOnly, setNeedsAttentionOnly] = React.useState(() => searchParams?.get('needsAttention') === 'true')
|
|
195
213
|
// One-shot URL hydration used as the hook's initial value. The hook is the
|
|
196
214
|
// single source of truth from this point on — the page MUST NOT keep a
|
|
197
215
|
// parallel `useState<AdvancedFilterTree>` (see spec "Migration & Backward
|
|
@@ -323,12 +341,13 @@ export default function CustomersDealsPage() {
|
|
|
323
341
|
if (search.trim().length) params.set('search', search.trim())
|
|
324
342
|
if (selectedPersonIds.length) params.set('personId', selectedPersonIds.join(','))
|
|
325
343
|
if (selectedCompanyIds.length) params.set('companyId', selectedCompanyIds.join(','))
|
|
344
|
+
if (needsAttentionOnly) params.set('needsAttention', 'true')
|
|
326
345
|
const advancedParams = serializeTree(advancedFilterState)
|
|
327
346
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
328
347
|
params.set(key, val)
|
|
329
348
|
}
|
|
330
349
|
return params.toString()
|
|
331
|
-
}, [advancedFilterState, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting])
|
|
350
|
+
}, [advancedFilterState, needsAttentionOnly, page, pageSize, search, selectedCompanyIds, selectedPersonIds, sorting])
|
|
332
351
|
|
|
333
352
|
const currentParams = React.useMemo(
|
|
334
353
|
() => Object.fromEntries(new URLSearchParams(queryParams)),
|
|
@@ -400,6 +419,7 @@ export default function CustomersDealsPage() {
|
|
|
400
419
|
if (search.trim().length) params.set('search', search.trim())
|
|
401
420
|
if (selectedPersonIds.length) selectedPersonIds.forEach((id) => params.append('personId', id))
|
|
402
421
|
if (selectedCompanyIds.length) selectedCompanyIds.forEach((id) => params.append('companyId', id))
|
|
422
|
+
if (needsAttentionOnly) params.set('needsAttention', 'true')
|
|
403
423
|
if (page > 1) params.set('page', String(page))
|
|
404
424
|
const advancedParams = serializeTree(advancedFilterState)
|
|
405
425
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
@@ -409,7 +429,7 @@ export default function CustomersDealsPage() {
|
|
|
409
429
|
if (queryRef.current === next) return
|
|
410
430
|
queryRef.current = next
|
|
411
431
|
router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false })
|
|
412
|
-
}, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, advancedFilterState])
|
|
432
|
+
}, [pathname, router, page, search, selectedPersonIds, selectedCompanyIds, needsAttentionOnly, advancedFilterState])
|
|
413
433
|
|
|
414
434
|
const handleRefresh = React.useCallback(() => {
|
|
415
435
|
void Promise.all([
|
|
@@ -498,6 +518,16 @@ export default function CustomersDealsPage() {
|
|
|
498
518
|
setPage(1)
|
|
499
519
|
}, [])
|
|
500
520
|
|
|
521
|
+
const handleNeedsAttentionFilter = React.useCallback(() => {
|
|
522
|
+
setNeedsAttentionOnly(true)
|
|
523
|
+
setPage(1)
|
|
524
|
+
}, [])
|
|
525
|
+
|
|
526
|
+
const handleNeedsAttentionClear = React.useCallback(() => {
|
|
527
|
+
setNeedsAttentionOnly(false)
|
|
528
|
+
setPage(1)
|
|
529
|
+
}, [])
|
|
530
|
+
|
|
501
531
|
const handleBulkDelete = React.useCallback(async (selectedRows: DealRow[]) => {
|
|
502
532
|
const confirmed = await confirm({
|
|
503
533
|
title: t('customers.deals.list.bulkDelete.title', 'Delete {count} deals?', { count: selectedRows.length }),
|
|
@@ -586,15 +616,27 @@ export default function CustomersDealsPage() {
|
|
|
586
616
|
})
|
|
587
617
|
const currentUserId = useCurrentUserId()
|
|
588
618
|
const [ownerFilterOptions, setOwnerFilterOptions] = React.useState<AdvancedFilterOption[]>([])
|
|
619
|
+
// Single staff load drives both the owner FILTER options and the owner-name
|
|
620
|
+
// map shared with the OWNER cell + the KPI strip (userId → display name).
|
|
621
|
+
// No per-row fetch — see spec audit "Owner names" resolution.
|
|
622
|
+
const [ownerNames, setOwnerNames] = React.useState<Record<string, string>>({})
|
|
589
623
|
React.useEffect(() => {
|
|
590
624
|
const controller = new AbortController()
|
|
591
625
|
let cancelled = false
|
|
592
626
|
void fetchAssignableStaffMembers('', { pageSize: 100, signal: controller.signal })
|
|
593
627
|
.then((items) => {
|
|
594
|
-
if (
|
|
628
|
+
if (cancelled) return
|
|
629
|
+
setOwnerFilterOptions(mapAssignableStaffToFilterOptions(items))
|
|
630
|
+
const names: Record<string, string> = {}
|
|
631
|
+
for (const item of items) {
|
|
632
|
+
if (item.userId) names[item.userId] = item.displayName
|
|
633
|
+
}
|
|
634
|
+
setOwnerNames(names)
|
|
595
635
|
})
|
|
596
636
|
.catch(() => {
|
|
597
|
-
if (
|
|
637
|
+
if (cancelled) return
|
|
638
|
+
setOwnerFilterOptions([])
|
|
639
|
+
setOwnerNames({})
|
|
598
640
|
})
|
|
599
641
|
return () => {
|
|
600
642
|
cancelled = true
|
|
@@ -614,32 +656,20 @@ export default function CustomersDealsPage() {
|
|
|
614
656
|
return mapAssignableStaffToFilterOptions(items)
|
|
615
657
|
}, [])
|
|
616
658
|
|
|
659
|
+
const startOfToday = React.useMemo(() => {
|
|
660
|
+
const today = new Date()
|
|
661
|
+
today.setHours(0, 0, 0, 0)
|
|
662
|
+
return today
|
|
663
|
+
}, [])
|
|
664
|
+
const isDealOverdue = React.useCallback(
|
|
665
|
+
(row: DealRow): boolean =>
|
|
666
|
+
!!row.expectedCloseAt && new Date(row.expectedCloseAt) < startOfToday && row.status === 'open',
|
|
667
|
+
[startOfToday],
|
|
668
|
+
)
|
|
669
|
+
|
|
617
670
|
const columns = React.useMemo<ColumnDef<DealRow>[]>(() => {
|
|
618
671
|
const noValue = <span className="text-muted-foreground text-sm">{t('customers.deals.list.noValue')}</span>
|
|
619
|
-
const
|
|
620
|
-
<DictionaryValue
|
|
621
|
-
value={value}
|
|
622
|
-
map={dictionaryMaps[kind]}
|
|
623
|
-
fallback={value ? <span className="text-sm">{value}</span> : noValue}
|
|
624
|
-
className="text-sm"
|
|
625
|
-
iconWrapperClassName="inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-card"
|
|
626
|
-
iconClassName="h-4 w-4"
|
|
627
|
-
colorClassName="h-3 w-3 rounded-full"
|
|
628
|
-
/>
|
|
629
|
-
)
|
|
630
|
-
const renderAssociationSummary = (
|
|
631
|
-
items: { id: string; label: string }[],
|
|
632
|
-
fallbackLabel: string,
|
|
633
|
-
) => {
|
|
634
|
-
if (!items.length) return noValue
|
|
635
|
-
const labels = normalizeCollectionLabels(
|
|
636
|
-
items.map((entry) => (entry.label && entry.label.trim().length ? entry.label : fallbackLabel)),
|
|
637
|
-
)
|
|
638
|
-
if (!labels.length) return noValue
|
|
639
|
-
return (
|
|
640
|
-
<CollectionPreviewCell labels={labels} maxVisible={1} />
|
|
641
|
-
)
|
|
642
|
-
}
|
|
672
|
+
const unknownOwner = t('customers.deals.list.unknownOwner')
|
|
643
673
|
|
|
644
674
|
const customColumns = customFieldDefs
|
|
645
675
|
.filter((def) => supportsCustomFieldColumn(def))
|
|
@@ -695,7 +725,20 @@ export default function CustomersDealsPage() {
|
|
|
695
725
|
filterGroup: 'Deal',
|
|
696
726
|
maxWidth: '280px',
|
|
697
727
|
},
|
|
698
|
-
cell: ({ row }) =>
|
|
728
|
+
cell: ({ row }) => (
|
|
729
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
730
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
|
|
731
|
+
<Briefcase className="h-4 w-4" />
|
|
732
|
+
</span>
|
|
733
|
+
<span className="font-medium text-foreground truncate">{row.original.title}</span>
|
|
734
|
+
{isDealOverdue(row.original) ? (
|
|
735
|
+
<AlertTriangle
|
|
736
|
+
className="h-4 w-4 shrink-0 text-status-warning-text"
|
|
737
|
+
aria-label={t('customers.deals.list.close.overdue')}
|
|
738
|
+
/>
|
|
739
|
+
) : null}
|
|
740
|
+
</div>
|
|
741
|
+
),
|
|
699
742
|
},
|
|
700
743
|
{
|
|
701
744
|
accessorKey: 'status',
|
|
@@ -707,7 +750,14 @@ export default function CustomersDealsPage() {
|
|
|
707
750
|
filterKey: 'status',
|
|
708
751
|
filterGroup: 'Deal',
|
|
709
752
|
},
|
|
710
|
-
cell: ({ row }) =>
|
|
753
|
+
cell: ({ row }) => {
|
|
754
|
+
const status = row.original.status
|
|
755
|
+
if (!status) return noValue
|
|
756
|
+
const entry = dictionaryMaps['deal-statuses']?.[status]
|
|
757
|
+
const label = entry?.label ?? status
|
|
758
|
+
const variant = coerceStatusBadgeVariant(mapDictionaryColorToTone(entry?.color))
|
|
759
|
+
return <StatusBadge variant={variant} dot>{label}</StatusBadge>
|
|
760
|
+
},
|
|
711
761
|
},
|
|
712
762
|
{
|
|
713
763
|
accessorKey: 'pipelineStage',
|
|
@@ -719,7 +769,12 @@ export default function CustomersDealsPage() {
|
|
|
719
769
|
filterKey: 'pipeline_stage',
|
|
720
770
|
filterGroup: 'Deal',
|
|
721
771
|
},
|
|
722
|
-
cell: ({ row }) =>
|
|
772
|
+
cell: ({ row }) => {
|
|
773
|
+
const stage = row.original.pipelineStage
|
|
774
|
+
if (!stage) return noValue
|
|
775
|
+
const label = dictionaryMaps['pipeline-stages']?.[stage]?.label ?? stage
|
|
776
|
+
return <span className="text-foreground">{label}</span>
|
|
777
|
+
},
|
|
723
778
|
},
|
|
724
779
|
{
|
|
725
780
|
accessorKey: 'pipelineId',
|
|
@@ -744,11 +799,17 @@ export default function CustomersDealsPage() {
|
|
|
744
799
|
filterKey: 'value_amount',
|
|
745
800
|
filterGroup: 'Deal',
|
|
746
801
|
},
|
|
747
|
-
cell: ({ row }) =>
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
802
|
+
cell: ({ row }) => {
|
|
803
|
+
const amount = formatGroupedAmount(row.original.valueAmount ?? null)
|
|
804
|
+
if (amount === null) return noValue
|
|
805
|
+
const currency = row.original.valueCurrency
|
|
806
|
+
return (
|
|
807
|
+
<div className="flex flex-col">
|
|
808
|
+
<span className="font-medium text-foreground">{amount}</span>
|
|
809
|
+
{currency ? <span className="text-xs text-muted-foreground">{currency}</span> : null}
|
|
810
|
+
</div>
|
|
811
|
+
)
|
|
812
|
+
},
|
|
752
813
|
},
|
|
753
814
|
{
|
|
754
815
|
accessorKey: 'probability',
|
|
@@ -762,7 +823,7 @@ export default function CustomersDealsPage() {
|
|
|
762
823
|
cell: ({ row }) => {
|
|
763
824
|
const value = row.original.probability
|
|
764
825
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
765
|
-
return <span className="text-
|
|
826
|
+
return <span className="font-medium text-foreground">{`${Math.min(Math.max(value, 0), 100)}%`}</span>
|
|
766
827
|
}
|
|
767
828
|
return noValue
|
|
768
829
|
},
|
|
@@ -776,11 +837,37 @@ export default function CustomersDealsPage() {
|
|
|
776
837
|
filterGroup: 'Activity',
|
|
777
838
|
filterIconName: 'calendar',
|
|
778
839
|
},
|
|
779
|
-
cell: ({ row }) =>
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
840
|
+
cell: ({ row }) => {
|
|
841
|
+
const expectedCloseAt = row.original.expectedCloseAt
|
|
842
|
+
if (!expectedCloseAt) return noValue
|
|
843
|
+
let subtitle: React.ReactNode = null
|
|
844
|
+
if (isDealOverdue(row.original)) {
|
|
845
|
+
subtitle = (
|
|
846
|
+
<span className="text-xs text-status-error-text">{t('customers.deals.list.close.overdue')}</span>
|
|
847
|
+
)
|
|
848
|
+
} else if (row.original.status === 'win') {
|
|
849
|
+
subtitle = (
|
|
850
|
+
<span className="text-xs text-muted-foreground">{t('customers.deals.list.close.won')}</span>
|
|
851
|
+
)
|
|
852
|
+
} else if (row.original.status === 'loose') {
|
|
853
|
+
subtitle = (
|
|
854
|
+
<span className="text-xs text-muted-foreground">{t('customers.deals.list.close.lost')}</span>
|
|
855
|
+
)
|
|
856
|
+
} else {
|
|
857
|
+
const relative = formatRelativeTime(expectedCloseAt, { translate: t })
|
|
858
|
+
if (relative) {
|
|
859
|
+
subtitle = <span className="text-xs text-muted-foreground">{relative}</span>
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return (
|
|
863
|
+
<div className="flex flex-col">
|
|
864
|
+
<span className="text-foreground">
|
|
865
|
+
{formatDateValue(expectedCloseAt, t('customers.deals.list.noValue'))}
|
|
866
|
+
</span>
|
|
867
|
+
{subtitle}
|
|
868
|
+
</div>
|
|
869
|
+
)
|
|
870
|
+
},
|
|
784
871
|
},
|
|
785
872
|
{
|
|
786
873
|
accessorKey: 'ownerUserId',
|
|
@@ -793,9 +880,18 @@ export default function CustomersDealsPage() {
|
|
|
793
880
|
filterGroup: 'CRM',
|
|
794
881
|
filterIconName: 'user-round',
|
|
795
882
|
filterKey: 'owner_user_id',
|
|
796
|
-
hidden: true,
|
|
797
883
|
},
|
|
798
|
-
cell: ({ row }) =>
|
|
884
|
+
cell: ({ row }) => {
|
|
885
|
+
const ownerUserId = row.original.ownerUserId
|
|
886
|
+
if (!ownerUserId) return noValue
|
|
887
|
+
const label = ownerNames[ownerUserId]?.trim() || unknownOwner
|
|
888
|
+
return (
|
|
889
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
890
|
+
<Avatar label={label} size="sm" />
|
|
891
|
+
<span className="text-foreground truncate">{label}</span>
|
|
892
|
+
</div>
|
|
893
|
+
)
|
|
894
|
+
},
|
|
799
895
|
},
|
|
800
896
|
{
|
|
801
897
|
accessorKey: 'companies',
|
|
@@ -811,7 +907,22 @@ export default function CustomersDealsPage() {
|
|
|
811
907
|
row.companies.map((entry) => (entry.label && entry.label.trim().length ? entry.label : t('customers.deals.list.unnamedCompany'))),
|
|
812
908
|
).join(', '),
|
|
813
909
|
},
|
|
814
|
-
cell: ({ row }) =>
|
|
910
|
+
cell: ({ row }) => {
|
|
911
|
+
const companies = row.original.companies
|
|
912
|
+
if (!companies.length) return noValue
|
|
913
|
+
const first = companies[0]
|
|
914
|
+
const firstLabel =
|
|
915
|
+
first.label && first.label.trim().length ? first.label : t('customers.deals.list.unnamedCompany')
|
|
916
|
+
const overflow = companies.length - 1
|
|
917
|
+
return (
|
|
918
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
919
|
+
<Tag variant="neutral" className="max-w-36">
|
|
920
|
+
<span className="truncate">{firstLabel}</span>
|
|
921
|
+
</Tag>
|
|
922
|
+
{overflow > 0 ? <Tag variant="neutral">{`+${overflow}`}</Tag> : null}
|
|
923
|
+
</div>
|
|
924
|
+
)
|
|
925
|
+
},
|
|
815
926
|
},
|
|
816
927
|
{
|
|
817
928
|
accessorKey: 'people',
|
|
@@ -827,7 +938,30 @@ export default function CustomersDealsPage() {
|
|
|
827
938
|
row.people.map((entry) => (entry.label && entry.label.trim().length ? entry.label : t('customers.deals.list.unnamedPerson'))),
|
|
828
939
|
).join(', '),
|
|
829
940
|
},
|
|
830
|
-
cell: ({ row }) =>
|
|
941
|
+
cell: ({ row }) => {
|
|
942
|
+
const people = row.original.people
|
|
943
|
+
if (!people.length) return noValue
|
|
944
|
+
const labels = normalizeCollectionLabels(
|
|
945
|
+
people.map((person) =>
|
|
946
|
+
person.label && person.label.trim().length ? person.label : t('customers.deals.list.unnamedPerson')),
|
|
947
|
+
)
|
|
948
|
+
const tooltip = labels.join(', ')
|
|
949
|
+
return (
|
|
950
|
+
<SimpleTooltip content={tooltip} side="top">
|
|
951
|
+
<span className="inline-flex">
|
|
952
|
+
<AvatarStack max={4} size="sm">
|
|
953
|
+
{people.map((person) => (
|
|
954
|
+
<Avatar
|
|
955
|
+
key={person.id}
|
|
956
|
+
label={person.label || t('customers.deals.list.unnamedPerson')}
|
|
957
|
+
size="sm"
|
|
958
|
+
/>
|
|
959
|
+
))}
|
|
960
|
+
</AvatarStack>
|
|
961
|
+
</span>
|
|
962
|
+
</SimpleTooltip>
|
|
963
|
+
)
|
|
964
|
+
},
|
|
831
965
|
},
|
|
832
966
|
{
|
|
833
967
|
accessorKey: 'updatedAt',
|
|
@@ -846,7 +980,7 @@ export default function CustomersDealsPage() {
|
|
|
846
980
|
},
|
|
847
981
|
...customColumns,
|
|
848
982
|
]
|
|
849
|
-
}, [customFieldDefs, dictionaryMaps, dictionaryOptions, loadOwnerFilterOptions, pipelineNames, resolvedOwnerFilterOptions, t])
|
|
983
|
+
}, [customFieldDefs, dictionaryMaps, dictionaryOptions, isDealOverdue, loadOwnerFilterOptions, ownerNames, pipelineNames, resolvedOwnerFilterOptions, t])
|
|
850
984
|
|
|
851
985
|
const { advancedFilterFields } = useAutoDiscoveredFields({ columns, customFieldDefs })
|
|
852
986
|
|
|
@@ -920,9 +1054,19 @@ export default function CustomersDealsPage() {
|
|
|
920
1054
|
<Page>
|
|
921
1055
|
<PageBody>
|
|
922
1056
|
<ViewTabsRow active="list" className="mb-4" />
|
|
1057
|
+
<DealsKpiStrip
|
|
1058
|
+
ownerNames={ownerNames}
|
|
1059
|
+
stageDictionary={dictionaryMaps['pipeline-stages'] ?? {}}
|
|
1060
|
+
pipelineCount={Object.keys(pipelineNames).length}
|
|
1061
|
+
scopeVersion={scopeVersion}
|
|
1062
|
+
reloadToken={reloadToken}
|
|
1063
|
+
onNeedsAttentionClick={handleNeedsAttentionFilter}
|
|
1064
|
+
className="mb-4"
|
|
1065
|
+
/>
|
|
923
1066
|
<DataTable<DealRow>
|
|
924
1067
|
stickyFirstColumn
|
|
925
1068
|
stickyActionsColumn
|
|
1069
|
+
actionsColumnAlign="center"
|
|
926
1070
|
title={t('customers.deals.list.title')}
|
|
927
1071
|
actions={(
|
|
928
1072
|
<Button asChild>
|
|
@@ -1016,6 +1160,29 @@ export default function CustomersDealsPage() {
|
|
|
1016
1160
|
}}
|
|
1017
1161
|
activeFilterChips={(
|
|
1018
1162
|
<>
|
|
1163
|
+
{needsAttentionOnly ? (
|
|
1164
|
+
<div
|
|
1165
|
+
className="flex items-center gap-2 overflow-x-auto border-b border-border bg-background px-4 py-2"
|
|
1166
|
+
data-testid="active-filter-chips"
|
|
1167
|
+
>
|
|
1168
|
+
<div
|
|
1169
|
+
className="inline-flex items-center gap-1"
|
|
1170
|
+
data-testid="active-filter-chip"
|
|
1171
|
+
aria-label={t('customers.deals.list.filters.needsAttention')}
|
|
1172
|
+
>
|
|
1173
|
+
<Tag variant="warning" dot>{t('customers.deals.list.filters.needsAttention')}</Tag>
|
|
1174
|
+
<IconButton
|
|
1175
|
+
type="button"
|
|
1176
|
+
variant="ghost"
|
|
1177
|
+
size="xs"
|
|
1178
|
+
aria-label={t('customers.deals.list.filters.needsAttentionRemove')}
|
|
1179
|
+
onClick={handleNeedsAttentionClear}
|
|
1180
|
+
>
|
|
1181
|
+
<X className="size-3" />
|
|
1182
|
+
</IconButton>
|
|
1183
|
+
</div>
|
|
1184
|
+
</div>
|
|
1185
|
+
) : null}
|
|
1019
1186
|
<ActiveFilterChips
|
|
1020
1187
|
tree={associationFilterTree}
|
|
1021
1188
|
fields={associationFilterFields}
|
|
@@ -1033,11 +1200,32 @@ export default function CustomersDealsPage() {
|
|
|
1033
1200
|
</>
|
|
1034
1201
|
)}
|
|
1035
1202
|
filterAwareEmptyState={{
|
|
1036
|
-
active: advancedFilterState.root.children.length > 0,
|
|
1203
|
+
active: needsAttentionOnly || associationFilterTree.root.children.length > 0 || advancedFilterState.root.children.length > 0,
|
|
1037
1204
|
entityNamePlural: t('customers.deals.entityPlural', 'deals'),
|
|
1038
|
-
canRemoveLast: filterPanel.tree.root.children.length > 0,
|
|
1039
|
-
onClearAll:
|
|
1040
|
-
|
|
1205
|
+
canRemoveLast: needsAttentionOnly || associationFilterTree.root.children.length > 0 || filterPanel.tree.root.children.length > 0,
|
|
1206
|
+
onClearAll: () => {
|
|
1207
|
+
handleAdvancedFilterClear()
|
|
1208
|
+
setSelectedPersonIds([])
|
|
1209
|
+
setSelectedCompanyIds([])
|
|
1210
|
+
setNeedsAttentionOnly(false)
|
|
1211
|
+
},
|
|
1212
|
+
onRemoveLast: () => {
|
|
1213
|
+
if (needsAttentionOnly) {
|
|
1214
|
+
handleNeedsAttentionClear()
|
|
1215
|
+
return
|
|
1216
|
+
}
|
|
1217
|
+
if (selectedCompanyIds.length > 0) {
|
|
1218
|
+
setSelectedCompanyIds([])
|
|
1219
|
+
setPage(1)
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1222
|
+
if (selectedPersonIds.length > 0) {
|
|
1223
|
+
setSelectedPersonIds([])
|
|
1224
|
+
setPage(1)
|
|
1225
|
+
return
|
|
1226
|
+
}
|
|
1227
|
+
filterPanel.dispatch({ type: 'removeLast' })
|
|
1228
|
+
},
|
|
1041
1229
|
}}
|
|
1042
1230
|
emptyState={(
|
|
1043
1231
|
<ListEmptyState
|
|
@@ -2542,6 +2542,7 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2542
2542
|
return (
|
|
2543
2543
|
<Page>
|
|
2544
2544
|
<PageBody>
|
|
2545
|
+
<ViewTabsRow active="kanban" className="mb-4" />
|
|
2545
2546
|
<div className="flex flex-col gap-2">
|
|
2546
2547
|
<Breadcrumb>
|
|
2547
2548
|
<BreadcrumbList>
|
|
@@ -2710,8 +2711,6 @@ export default function DealsKanbanPage(): React.ReactElement {
|
|
|
2710
2711
|
) : null}
|
|
2711
2712
|
</div>
|
|
2712
2713
|
|
|
2713
|
-
<ViewTabsRow active="kanban" className="mt-4" />
|
|
2714
|
-
|
|
2715
2714
|
<FilterBarRow
|
|
2716
2715
|
leadingChips={leadingChipsNode}
|
|
2717
2716
|
chips={filterChips}
|
|
@@ -37,6 +37,7 @@ import { ScheduleActivityDialog, type ScheduleActivityEditData } from '../../../
|
|
|
37
37
|
import { PersonDetailHeader } from '../../../../components/detail/PersonDetailHeader'
|
|
38
38
|
import { ChangelogTab } from '../../../../components/detail/ChangelogTab'
|
|
39
39
|
import { PersonDetailTabs, resolveLegacyTab, type PersonTabId } from '../../../../components/detail/PersonDetailTabs'
|
|
40
|
+
import { AddressesSection } from '../../../../components/detail/AddressesSection'
|
|
40
41
|
import { PersonCompaniesSection } from '../../../../components/detail/PersonCompaniesSection'
|
|
41
42
|
import { MobilePersonDetail } from '../../../../components/detail/MobilePersonDetail'
|
|
42
43
|
import type { TagsSectionController } from '@open-mercato/ui/backend/detail'
|
|
@@ -541,6 +542,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
541
542
|
activitiesCount={interactionCount}
|
|
542
543
|
dealsCount={dealCount}
|
|
543
544
|
companiesCount={companyCount}
|
|
545
|
+
addressesCount={data?.counts?.addresses ?? 0}
|
|
544
546
|
tasksCount={todoCount}
|
|
545
547
|
sectionAction={sectionAction}
|
|
546
548
|
>
|
|
@@ -619,6 +621,22 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
619
621
|
)
|
|
620
622
|
}
|
|
621
623
|
|
|
624
|
+
if (activeTab === 'addresses') {
|
|
625
|
+
return (
|
|
626
|
+
<AddressesSection
|
|
627
|
+
entityId={personId}
|
|
628
|
+
emptyLabel={t('customers.people.detail.empty.addresses', 'No addresses linked to this person.')}
|
|
629
|
+
addActionLabel={t('customers.people.detail.addresses.add', 'Add address')}
|
|
630
|
+
emptyState={{
|
|
631
|
+
title: t('customers.people.detail.emptyState.addresses.title', 'No addresses yet'),
|
|
632
|
+
actionLabel: t('customers.people.detail.emptyState.addresses.action', 'Add address'),
|
|
633
|
+
}}
|
|
634
|
+
onActionChange={handleSectionActionChange}
|
|
635
|
+
translator={detailTranslator}
|
|
636
|
+
/>
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
622
640
|
if (activeTab === 'tasks') {
|
|
623
641
|
return (
|
|
624
642
|
<TasksSection
|