@open-mercato/core 0.6.8-develop.7047.1.32ab5fcb07 → 0.6.8-develop.7049.1.b41427429a

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.
@@ -3,6 +3,7 @@ import { z } from 'zod'
3
3
  import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
4
4
  import { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
5
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
6
+ import { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'
6
7
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
7
8
  import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
8
9
  import { createCustomersCrudOpenApi, createPagedListResponseSchema } from '../../openapi'
@@ -40,11 +41,30 @@ const MERGED_TASK_FETCH_CAP = 2000
40
41
  export async function GET(request: Request): Promise<Response> {
41
42
  const { translate } = await resolveTranslations()
42
43
  try {
43
- const { auth, em, organizationIds, container, selectedOrganizationId } =
44
+ const { auth, em, organizationIds, container, selectedOrganizationId, scope } =
44
45
  await resolveCustomersRequestContext(request)
45
46
  const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))
46
47
  const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)
47
48
  const exportAll = parseBooleanToken(query.all) === true
49
+ const isUnrestricted = isUnrestrictedOrganizationScope({
50
+ isSuperAdmin: auth.isSuperAdmin === true,
51
+ allowedOrganizationIds: scope?.allowedIds,
52
+ })
53
+ if (!isUnrestricted && (!organizationIds || organizationIds.length === 0)) {
54
+ logger.warn('customers.interactions.tasks.list collapsed organization scope', {
55
+ tenantId: auth.tenantId,
56
+ organizationIds,
57
+ selectedId: scope?.selectedId ?? null,
58
+ allowedIdsCount: scope?.allowedIds?.length ?? null,
59
+ })
60
+ return NextResponse.json({
61
+ items: [],
62
+ total: 0,
63
+ page: exportAll ? 1 : query.page,
64
+ pageSize: exportAll ? 0 : query.pageSize,
65
+ totalPages: 1,
66
+ })
67
+ }
48
68
  const search = normalizeTodoSearch(query.search)
49
69
  const queryEngine = container.resolve('queryEngine') as QueryEngine
50
70
 
@@ -59,6 +79,7 @@ export async function GET(request: Request): Promise<Response> {
59
79
  entityId: query.entityId,
60
80
  pagination: exportAll ? null : { page: query.page, pageSize: query.pageSize },
61
81
  searchText: search,
82
+ isUnrestricted,
62
83
  },
63
84
  )
64
85
  const total = canonical.total
@@ -80,6 +101,7 @@ export async function GET(request: Request): Promise<Response> {
80
101
  const [legacyRows, canonicalRows] = await Promise.all([
81
102
  listLegacyTodoRows(em, queryEngine, auth.tenantId, organizationIds, query.entityId, {
82
103
  limit: legacyWindow,
104
+ isUnrestricted,
83
105
  }),
84
106
  listCanonicalTodoRows(
85
107
  em,
@@ -91,6 +113,7 @@ export async function GET(request: Request): Promise<Response> {
91
113
  entityId: query.entityId,
92
114
  includeDeleted: true,
93
115
  limit: legacyWindow,
116
+ isUnrestricted,
94
117
  },
95
118
  ),
96
119
  ])
@@ -9,6 +9,7 @@ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
9
9
  import type { CommandBus } from '@open-mercato/shared/lib/commands'
10
10
  import { isCrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'
11
11
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
12
+ import { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'
12
13
  import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
13
14
  import {
14
15
  runCrudMutationGuardAfterSuccess,
@@ -233,15 +234,36 @@ async function resolveCanonicalTodoTargetId(
233
234
 
234
235
  export async function GET(request: Request): Promise<Response> {
235
236
  try {
236
- const { auth, em, organizationIds, container, selectedOrganizationId } =
237
+ const { auth, em, organizationIds, container, selectedOrganizationId, scope } =
237
238
  await resolveCustomersRequestContext(request)
238
239
  const query = querySchema.parse(Object.fromEntries(new URL(request.url).searchParams))
239
240
  const flags = await resolveCustomerInteractionFeatureFlags(container, auth.tenantId)
240
241
  if (!flags.legacyAdapters) {
241
242
  return await legacyAdaptersDisabledResponse()
242
243
  }
243
- const queryEngine = container.resolve('queryEngine') as QueryEngine
244
244
  const exportAll = parseBooleanToken(query.all) === true
245
+ const isUnrestricted = isUnrestrictedOrganizationScope({
246
+ isSuperAdmin: auth.isSuperAdmin === true,
247
+ allowedOrganizationIds: scope?.allowedIds,
248
+ })
249
+ if (!isUnrestricted && (!organizationIds || organizationIds.length === 0)) {
250
+ logger.warn('customers.todos.list collapsed organization scope', {
251
+ tenantId: auth.tenantId,
252
+ organizationIds,
253
+ selectedId: scope?.selectedId ?? null,
254
+ allowedIdsCount: scope?.allowedIds?.length ?? null,
255
+ })
256
+ return withAdapterHeaders(
257
+ NextResponse.json({
258
+ items: [],
259
+ total: 0,
260
+ page: exportAll ? 1 : query.page,
261
+ pageSize: exportAll ? 0 : query.pageSize,
262
+ totalPages: 1,
263
+ }),
264
+ )
265
+ }
266
+ const queryEngine = container.resolve('queryEngine') as QueryEngine
245
267
  const search = normalizeTodoSearch(query.search)
246
268
 
247
269
  if (flags.unified) {
@@ -255,6 +277,7 @@ export async function GET(request: Request): Promise<Response> {
255
277
  entityId: query.entityId,
256
278
  pagination: exportAll ? null : { page: query.page, pageSize: query.pageSize },
257
279
  searchText: search,
280
+ isUnrestricted,
258
281
  },
259
282
  )
260
283
  const total = canonical.total
@@ -275,6 +298,7 @@ export async function GET(request: Request): Promise<Response> {
275
298
  const [legacyRows, canonicalRows] = await Promise.all([
276
299
  listLegacyTodoRows(em, queryEngine, auth.tenantId, organizationIds, query.entityId, {
277
300
  limit: legacyWindow,
301
+ isUnrestricted,
278
302
  }),
279
303
  listCanonicalTodoRows(
280
304
  em,
@@ -287,6 +311,7 @@ export async function GET(request: Request): Promise<Response> {
287
311
  includeDeleted: true,
288
312
  source: CUSTOMER_INTERACTION_TODO_ADAPTER_SOURCE,
289
313
  limit: legacyWindow,
314
+ isUnrestricted,
290
315
  },
291
316
  ),
292
317
  ])
@@ -3,7 +3,7 @@
3
3
  import * as React from 'react'
4
4
  import { extensionPoints } from '@open-mercato/core/modules/customers/extension-points'
5
5
  import Link from 'next/link'
6
- import { Building2, UserSearch, Users } from 'lucide-react'
6
+ import { Building2, FileText, Hash, UserSearch, Users } from 'lucide-react'
7
7
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
8
8
  import { useRouter, useSearchParams, usePathname } from 'next/navigation'
9
9
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
@@ -12,7 +12,7 @@ import { AttachmentsSection, ErrorMessage, LoadingMessage, NotesSection, RecordN
12
12
  import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
13
13
  import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
14
14
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
15
- import { CollapsibleZoneLayout } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
15
+ import { CollapsibleZoneLayout, type ZoneSectionDescriptor } from '@open-mercato/ui/backend/crud/CollapsibleZoneLayout'
16
16
  import { useT } from '@open-mercato/shared/lib/i18n/context'
17
17
  import { createTranslatorWithFallback } from '@open-mercato/shared/lib/i18n/translate'
18
18
  import { E } from '#generated/entities.ids.generated'
@@ -57,6 +57,10 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
57
57
  const pathname = usePathname()
58
58
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
59
59
  const detailTranslator = React.useMemo(() => createTranslatorWithFallback(t), [t])
60
+ const zoneSections = React.useMemo<ZoneSectionDescriptor[]>(() => [
61
+ { id: 'details', icon: FileText, label: t('customers.people.detail.deals.form.details', 'Deal details') },
62
+ { id: 'custom', icon: Hash, label: t('customers.people.detail.deals.form.customFields', 'Custom fields') },
63
+ ], [t])
60
64
 
61
65
  const { data, setData, isLoading, error, isNotFound, loadData } = useDealData(id)
62
66
  const [isDirty, setIsDirty] = React.useState(false)
@@ -664,6 +668,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
664
668
  pageType="deal-detail-v3"
665
669
  entityName={dealName}
666
670
  isDirty={isDirty}
671
+ sections={zoneSections}
667
672
  zone1DefaultWidth="540px"
668
673
  zone1={zone1Content}
669
674
  zone2={zone2Content}
@@ -78,6 +78,17 @@ export type CanonicalTodoListResult = {
78
78
  total: number
79
79
  }
80
80
 
81
+ function emptyCanonicalTodoList(): CanonicalTodoListResult {
82
+ return { items: [], bridgeIds: new Set(), total: 0 }
83
+ }
84
+
85
+ function isCollapsedRestrictedOrganizationScope(
86
+ organizationIds: string[] | null | undefined,
87
+ isUnrestricted: boolean | undefined,
88
+ ): boolean {
89
+ return isUnrestricted !== true && (!organizationIds || organizationIds.length === 0)
90
+ }
91
+
81
92
  export type ListTodosPagination = { page: number; pageSize: number }
82
93
 
83
94
  function resolveLegacyTodoSource(source: string | null | undefined): string {
@@ -377,8 +388,11 @@ export async function listLegacyTodoRows(
377
388
  tenantId: string,
378
389
  organizationIds: string[] | null,
379
390
  entityId: string | undefined,
380
- options?: { limit?: number | null },
391
+ options?: { limit?: number | null; isUnrestricted?: boolean },
381
392
  ): Promise<CustomerTodoRow[]> {
393
+ if (isCollapsedRestrictedOrganizationScope(organizationIds, options?.isUnrestricted)) {
394
+ return []
395
+ }
382
396
  const where: Record<string, unknown> = { tenantId }
383
397
  if (organizationIds && organizationIds.length > 0) {
384
398
  where.organizationId = { $in: organizationIds }
@@ -425,8 +439,12 @@ export async function listCanonicalTodoRows(
425
439
  pagination?: ListTodosPagination | null
426
440
  searchText?: string | null
427
441
  limit?: number | null
442
+ isUnrestricted?: boolean
428
443
  },
429
444
  ): Promise<CanonicalTodoListResult> {
445
+ if (isCollapsedRestrictedOrganizationScope(organizationIds, options?.isUnrestricted)) {
446
+ return emptyCanonicalTodoList()
447
+ }
430
448
  const where: Record<string, unknown> = {
431
449
  tenantId: auth.tenantId,
432
450
  interactionType: 'task',
@@ -1,6 +1,7 @@
1
1
  import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { createRequestContainer, type AppContainer } from '@open-mercato/shared/lib/di/container'
3
3
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
4
+ import { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'
4
5
  import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
5
6
  import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
7
 
@@ -9,6 +10,7 @@ export type WidgetScopeContext = {
9
10
  em: EntityManager
10
11
  tenantId: string
11
12
  organizationIds: string[] | null
13
+ isUnrestrictedOrganizationScope?: boolean
12
14
  }
13
15
 
14
16
  function normalizeScopeId(value: string | null | undefined): string | null {
@@ -91,5 +93,9 @@ export async function resolveWidgetScope(
91
93
  em,
92
94
  tenantId,
93
95
  organizationIds,
96
+ isUnrestrictedOrganizationScope: isUnrestrictedOrganizationScope({
97
+ isSuperAdmin,
98
+ allowedOrganizationIds: scope?.allowedIds,
99
+ }),
94
100
  }
95
101
  }