@open-mercato/core 0.6.4-develop.4000.1.450e315cec → 0.6.4-develop.4015.1.efaafadf79
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/modules/auth/backend/users/[id]/edit/page.js +70 -57
- package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
- package/dist/modules/catalog/acl.js +30 -5
- package/dist/modules/catalog/acl.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -5
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/commands/offers.js +26 -7
- package/dist/modules/catalog/commands/offers.js.map +2 -2
- package/dist/modules/catalog/commands/prices.js +41 -26
- package/dist/modules/catalog/commands/prices.js.map +2 -2
- package/dist/modules/catalog/commands/productUnitConversions.js +7 -1
- package/dist/modules/catalog/commands/productUnitConversions.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +2 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/commands/shared.js +58 -11
- package/dist/modules/catalog/commands/shared.js.map +2 -2
- package/dist/modules/catalog/commands/variants.js +18 -5
- package/dist/modules/catalog/commands/variants.js.map +2 -2
- package/dist/modules/customers/api/companies/route.js +6 -0
- package/dist/modules/customers/api/companies/route.js.map +2 -2
- package/dist/modules/customers/api/people/route.js +6 -0
- package/dist/modules/customers/api/people/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +10 -9
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/listSorting.js +28 -0
- package/dist/modules/customers/backend/customers/listSorting.js.map +7 -0
- package/dist/modules/customers/backend/customers/people/page.js +10 -9
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +17 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +20 -1
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/backend/users/[id]/edit/page.tsx +28 -6
- package/src/modules/auth/i18n/de.json +1 -0
- package/src/modules/auth/i18n/en.json +1 -0
- package/src/modules/auth/i18n/es.json +1 -0
- package/src/modules/auth/i18n/pl.json +1 -0
- package/src/modules/catalog/acl.ts +30 -5
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +21 -5
- package/src/modules/catalog/commands/offers.ts +26 -7
- package/src/modules/catalog/commands/prices.ts +41 -26
- package/src/modules/catalog/commands/productUnitConversions.ts +7 -1
- package/src/modules/catalog/commands/products.ts +2 -0
- package/src/modules/catalog/commands/shared.ts +70 -6
- package/src/modules/catalog/commands/variants.ts +18 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customers/api/companies/route.ts +6 -0
- package/src/modules/customers/api/people/route.ts +6 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +12 -11
- package/src/modules/customers/backend/customers/listSorting.ts +27 -0
- package/src/modules/customers/backend/customers/people/page.tsx +12 -11
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +21 -2
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +28 -1
- package/src/modules/sales/i18n/de.json +3 -0
- package/src/modules/sales/i18n/en.json +3 -0
- package/src/modules/sales/i18n/es.json +3 -0
- package/src/modules/sales/i18n/pl.json +3 -0
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
CatalogOptionSchemaTemplate,
|
|
6
6
|
CatalogPriceKind,
|
|
7
7
|
} from '../data/entities'
|
|
8
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
8
|
+
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
9
9
|
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
10
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
11
11
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -73,12 +73,42 @@ export function toNumericString(value: number | null | undefined): string | null
|
|
|
73
73
|
return value.toString()
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export type RequireScope = {
|
|
77
|
+
tenantId: string | null
|
|
78
|
+
organizationId: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Derives the actor's effective tenant/org scope for entry-point lookups, mirroring
|
|
82
|
+
// the bypass semantics of ensureTenantScope/ensureOrganizationScope: tenant is always
|
|
83
|
+
// strict, organization is left unrestricted for super-admins and global-org actors.
|
|
84
|
+
export function commandActorScope(ctx: CommandRuntimeContext): RequireScope {
|
|
85
|
+
const orgUnrestricted = ctx.auth?.isSuperAdmin === true || ctx.organizationScope?.allowedIds === null
|
|
86
|
+
return {
|
|
87
|
+
tenantId: ctx.auth?.tenantId ?? null,
|
|
88
|
+
organizationId: orgUnrestricted ? null : (ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyScopeToWhere(where: Record<string, unknown>, scope: RequireScope): void {
|
|
93
|
+
if (scope.tenantId != null) where.tenantId = scope.tenantId
|
|
94
|
+
if (scope.organizationId != null) where.organizationId = scope.organizationId
|
|
95
|
+
}
|
|
96
|
+
|
|
76
97
|
export async function requireProduct(
|
|
77
98
|
em: EntityManager,
|
|
78
99
|
id: string,
|
|
100
|
+
scope: RequireScope,
|
|
79
101
|
message = 'Catalog product not found'
|
|
80
102
|
): Promise<CatalogProduct> {
|
|
81
|
-
const
|
|
103
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
104
|
+
applyScopeToWhere(where, scope)
|
|
105
|
+
const product = await findOneWithDecryption(
|
|
106
|
+
em,
|
|
107
|
+
CatalogProduct,
|
|
108
|
+
where as FilterQuery<CatalogProduct>,
|
|
109
|
+
undefined,
|
|
110
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
111
|
+
)
|
|
82
112
|
if (!product) throw new CrudHttpError(404, { error: message })
|
|
83
113
|
return product
|
|
84
114
|
}
|
|
@@ -86,13 +116,17 @@ export async function requireProduct(
|
|
|
86
116
|
export async function requireVariant(
|
|
87
117
|
em: EntityManager,
|
|
88
118
|
id: string,
|
|
119
|
+
scope: RequireScope,
|
|
89
120
|
message = 'Catalog variant not found'
|
|
90
121
|
): Promise<CatalogProductVariant> {
|
|
122
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
123
|
+
applyScopeToWhere(where, scope)
|
|
91
124
|
const variant = await findOneWithDecryption(
|
|
92
125
|
em,
|
|
93
126
|
CatalogProductVariant,
|
|
94
|
-
|
|
127
|
+
where as FilterQuery<CatalogProductVariant>,
|
|
95
128
|
{ populate: ['product'] },
|
|
129
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
96
130
|
)
|
|
97
131
|
if (!variant) throw new CrudHttpError(404, { error: message })
|
|
98
132
|
return variant
|
|
@@ -101,9 +135,18 @@ export async function requireVariant(
|
|
|
101
135
|
export async function requireOffer(
|
|
102
136
|
em: EntityManager,
|
|
103
137
|
id: string,
|
|
138
|
+
scope: RequireScope,
|
|
104
139
|
message = 'Catalog offer not found'
|
|
105
140
|
): Promise<CatalogOffer> {
|
|
106
|
-
const
|
|
141
|
+
const where: Record<string, unknown> = { id }
|
|
142
|
+
applyScopeToWhere(where, scope)
|
|
143
|
+
const offer = await findOneWithDecryption(
|
|
144
|
+
em,
|
|
145
|
+
CatalogOffer,
|
|
146
|
+
where as FilterQuery<CatalogOffer>,
|
|
147
|
+
undefined,
|
|
148
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
149
|
+
)
|
|
107
150
|
if (!offer) throw new CrudHttpError(404, { error: message })
|
|
108
151
|
return offer
|
|
109
152
|
}
|
|
@@ -111,9 +154,21 @@ export async function requireOffer(
|
|
|
111
154
|
export async function requirePriceKind(
|
|
112
155
|
em: EntityManager,
|
|
113
156
|
id: string,
|
|
157
|
+
scope: RequireScope,
|
|
114
158
|
message = 'Catalog price kind not found'
|
|
115
159
|
): Promise<CatalogPriceKind> {
|
|
116
|
-
|
|
160
|
+
// Price kinds are tenant-global: organization_id is always null and the unique key is
|
|
161
|
+
// (tenant_id, code). Scope by tenant only — applying a concrete org would never match the
|
|
162
|
+
// null row. Tenant scoping still closes the cross-tenant read hole this helper guards.
|
|
163
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
164
|
+
applyScopeToWhere(where, { tenantId: scope.tenantId, organizationId: null })
|
|
165
|
+
const priceKind = await findOneWithDecryption(
|
|
166
|
+
em,
|
|
167
|
+
CatalogPriceKind,
|
|
168
|
+
where as FilterQuery<CatalogPriceKind>,
|
|
169
|
+
undefined,
|
|
170
|
+
{ tenantId: scope.tenantId, organizationId: null },
|
|
171
|
+
)
|
|
117
172
|
if (!priceKind) throw new CrudHttpError(404, { error: message })
|
|
118
173
|
return priceKind
|
|
119
174
|
}
|
|
@@ -121,9 +176,18 @@ export async function requirePriceKind(
|
|
|
121
176
|
export async function requireOptionSchemaTemplate(
|
|
122
177
|
em: EntityManager,
|
|
123
178
|
id: string,
|
|
179
|
+
scope: RequireScope,
|
|
124
180
|
message = 'Option schema not found'
|
|
125
181
|
): Promise<CatalogOptionSchemaTemplate> {
|
|
126
|
-
const
|
|
182
|
+
const where: Record<string, unknown> = { id, deletedAt: null }
|
|
183
|
+
applyScopeToWhere(where, scope)
|
|
184
|
+
const schema = await findOneWithDecryption(
|
|
185
|
+
em,
|
|
186
|
+
CatalogOptionSchemaTemplate,
|
|
187
|
+
where as FilterQuery<CatalogOptionSchemaTemplate>,
|
|
188
|
+
undefined,
|
|
189
|
+
{ tenantId: scope.tenantId, organizationId: scope.organizationId },
|
|
190
|
+
)
|
|
127
191
|
if (!schema) throw new CrudHttpError(404, { error: message })
|
|
128
192
|
return schema
|
|
129
193
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from '../data/validators'
|
|
26
26
|
import {
|
|
27
27
|
cloneJson,
|
|
28
|
+
commandActorScope,
|
|
28
29
|
ensureOrganizationScope,
|
|
29
30
|
ensureTenantScope,
|
|
30
31
|
emitCatalogQueryIndexEvent,
|
|
@@ -318,7 +319,10 @@ async function restoreVariantPricesFromSnapshots(
|
|
|
318
319
|
if (!snapshots.length) return
|
|
319
320
|
const productRef =
|
|
320
321
|
typeof variant.product === 'string'
|
|
321
|
-
? await requireProduct(em, variant.product
|
|
322
|
+
? await requireProduct(em, variant.product, {
|
|
323
|
+
tenantId: variant.tenantId,
|
|
324
|
+
organizationId: variant.organizationId,
|
|
325
|
+
})
|
|
322
326
|
: variant.product
|
|
323
327
|
for (const snapshot of snapshots) {
|
|
324
328
|
const product =
|
|
@@ -547,7 +551,7 @@ const createVariantCommand: CommandHandler<VariantCreateInput, { variantId: stri
|
|
|
547
551
|
async execute(rawInput, ctx) {
|
|
548
552
|
const { parsed, custom } = parseWithCustomFields(variantCreateSchema, rawInput)
|
|
549
553
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
550
|
-
const product = await requireProduct(em, parsed.productId)
|
|
554
|
+
const product = await requireProduct(em, parsed.productId, commandActorScope(ctx))
|
|
551
555
|
ensureTenantScope(ctx, product.tenantId)
|
|
552
556
|
ensureOrganizationScope(ctx, product.organizationId)
|
|
553
557
|
const { taxRateId, taxRate } = await resolveVariantTaxRate(
|
|
@@ -705,7 +709,10 @@ const updateVariantCommand: CommandHandler<VariantUpdateInput, { variantId: stri
|
|
|
705
709
|
if (!record) throw new CrudHttpError(404, { error: 'Catalog variant not found' })
|
|
706
710
|
ensureTenantScope(ctx, record.tenantId)
|
|
707
711
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
708
|
-
const product = await requireProduct(em, record.product.id
|
|
712
|
+
const product = await requireProduct(em, record.product.id, {
|
|
713
|
+
tenantId: record.tenantId,
|
|
714
|
+
organizationId: record.organizationId,
|
|
715
|
+
})
|
|
709
716
|
|
|
710
717
|
if (!product) throw new CrudHttpError(400, { error: 'Variant product missing' })
|
|
711
718
|
|
|
@@ -827,7 +834,10 @@ const updateVariantCommand: CommandHandler<VariantUpdateInput, { variantId: stri
|
|
|
827
834
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
828
835
|
let record = await em.findOne(CatalogProductVariant, { id: before.id })
|
|
829
836
|
if (!record) {
|
|
830
|
-
const product = await requireProduct(em, before.productId
|
|
837
|
+
const product = await requireProduct(em, before.productId, {
|
|
838
|
+
tenantId: before.tenantId,
|
|
839
|
+
organizationId: before.organizationId,
|
|
840
|
+
})
|
|
831
841
|
record = em.create(CatalogProductVariant, {
|
|
832
842
|
id: before.id,
|
|
833
843
|
product,
|
|
@@ -994,7 +1004,10 @@ const deleteVariantCommand: CommandHandler<
|
|
|
994
1004
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
995
1005
|
let record = await em.findOne(CatalogProductVariant, { id: before.id })
|
|
996
1006
|
if (!record) {
|
|
997
|
-
const product = await requireProduct(em, before.productId
|
|
1007
|
+
const product = await requireProduct(em, before.productId, {
|
|
1008
|
+
tenantId: before.tenantId,
|
|
1009
|
+
organizationId: before.organizationId,
|
|
1010
|
+
})
|
|
998
1011
|
record = em.create(CatalogProductVariant, {
|
|
999
1012
|
id: before.id,
|
|
1000
1013
|
product,
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Steuerklasse",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Produkt-Steuerklasse verwenden ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Keine Steuerklasse",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Zurück zu Produkten",
|
|
364
365
|
"catalog.products.edit.custom.title": "Benutzerdefinierte Attribute",
|
|
365
366
|
"catalog.products.edit.dimensions": "Maße & Gewicht",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Tiefe",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Tax class",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Use product tax class ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "No tax class",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Back to products",
|
|
364
365
|
"catalog.products.edit.custom.title": "Custom attributes",
|
|
365
366
|
"catalog.products.edit.dimensions": "Dimensions & weight",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Depth",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Clase de impuesto",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Usar clase de impuesto del producto ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Sin clase de impuesto",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Volver a productos",
|
|
364
365
|
"catalog.products.edit.custom.title": "Atributos personalizados",
|
|
365
366
|
"catalog.products.edit.dimensions": "Dimensiones y peso",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Profundidad",
|
|
@@ -361,6 +361,7 @@
|
|
|
361
361
|
"catalog.products.create.variantsBuilder.vatColumn": "Klasa podatkowa",
|
|
362
362
|
"catalog.products.create.variantsBuilder.vatOptionDefault": "Użyj klasy podatkowej produktu ({{label}})",
|
|
363
363
|
"catalog.products.create.variantsBuilder.vatOptionNone": "Brak klasy podatkowej",
|
|
364
|
+
"catalog.products.edit.actions.backToList": "Powrót do listy produktów",
|
|
364
365
|
"catalog.products.edit.custom.title": "Atrybuty niestandardowe",
|
|
365
366
|
"catalog.products.edit.dimensions": "Wymiary i waga",
|
|
366
367
|
"catalog.products.edit.dimensions.depth": "Głębokość",
|
|
@@ -114,6 +114,12 @@ const crud = makeCrudRoute({
|
|
|
114
114
|
],
|
|
115
115
|
sortFieldMap: {
|
|
116
116
|
name: 'display_name',
|
|
117
|
+
email: 'primary_email',
|
|
118
|
+
primaryEmail: 'primary_email',
|
|
119
|
+
status: 'status',
|
|
120
|
+
lifecycleStage: 'lifecycle_stage',
|
|
121
|
+
source: 'source',
|
|
122
|
+
nextInteractionAt: 'next_interaction_at',
|
|
117
123
|
createdAt: 'created_at',
|
|
118
124
|
updatedAt: 'updated_at',
|
|
119
125
|
},
|
|
@@ -108,6 +108,12 @@ const crud = makeCrudRoute({
|
|
|
108
108
|
],
|
|
109
109
|
sortFieldMap: {
|
|
110
110
|
name: 'display_name',
|
|
111
|
+
email: 'primary_email',
|
|
112
|
+
primaryEmail: 'primary_email',
|
|
113
|
+
status: 'status',
|
|
114
|
+
lifecycleStage: 'lifecycle_stage',
|
|
115
|
+
source: 'source',
|
|
116
|
+
nextInteractionAt: 'next_interaction_at',
|
|
111
117
|
createdAt: 'created_at',
|
|
112
118
|
updatedAt: 'updated_at',
|
|
113
119
|
},
|
|
@@ -5,7 +5,7 @@ import Link from 'next/link'
|
|
|
5
5
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { DataTable, type DataTableExportFormat, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
|
|
8
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
8
|
+
import type { ColumnDef, SortingState } from '@tanstack/react-table'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
10
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
11
11
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
mapAssignableStaffToFilterOptions,
|
|
54
54
|
} from '../../../components/detail/assignableStaff'
|
|
55
55
|
import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
|
|
56
|
+
import { appendCustomerListSortParams } from '../listSorting'
|
|
56
57
|
|
|
57
58
|
type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
|
|
58
59
|
|
|
@@ -191,7 +192,7 @@ export default function CustomersCompaniesPage() {
|
|
|
191
192
|
const [rows, setRows] = React.useState<CompanyRow[]>([])
|
|
192
193
|
const [page, setPage] = React.useState(1)
|
|
193
194
|
const [pageSize, setPageSize] = React.useState(20)
|
|
194
|
-
const [sorting, setSorting] = React.useState<
|
|
195
|
+
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
195
196
|
const [total, setTotal] = React.useState(0)
|
|
196
197
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
197
198
|
const [search, setSearch] = React.useState('')
|
|
@@ -246,6 +247,10 @@ export default function CustomersCompaniesPage() {
|
|
|
246
247
|
setPageSize(newSize)
|
|
247
248
|
setPage(1)
|
|
248
249
|
}, [])
|
|
250
|
+
const handleSortingChange = React.useCallback((nextSorting: SortingState) => {
|
|
251
|
+
setSorting(nextSorting)
|
|
252
|
+
setPage(1)
|
|
253
|
+
}, [])
|
|
249
254
|
|
|
250
255
|
const bulkMutationContextId = 'customers-companies-list:bulk-delete'
|
|
251
256
|
const { runMutation: runBulkMutation, retryLastMutation: retryBulkMutation } = useGuardedMutation<{
|
|
@@ -350,10 +355,7 @@ export default function CustomersCompaniesPage() {
|
|
|
350
355
|
const params = new URLSearchParams()
|
|
351
356
|
params.set('page', String(page))
|
|
352
357
|
params.set('pageSize', String(pageSize))
|
|
353
|
-
|
|
354
|
-
params.set('sort', sorting[0].id)
|
|
355
|
-
params.set('order', sorting[0].desc ? 'desc' : 'asc')
|
|
356
|
-
}
|
|
358
|
+
appendCustomerListSortParams(params, sorting)
|
|
357
359
|
if (search.trim()) params.set('search', search.trim())
|
|
358
360
|
const advancedParams = serializeTree(advancedFilterState)
|
|
359
361
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
@@ -374,10 +376,7 @@ export default function CustomersCompaniesPage() {
|
|
|
374
376
|
const params = new URLSearchParams()
|
|
375
377
|
if (search.trim().length) params.set('search', search.trim())
|
|
376
378
|
if (page > 1) params.set('page', String(page))
|
|
377
|
-
|
|
378
|
-
params.set('sort', sorting[0].id)
|
|
379
|
-
params.set('order', sorting[0].desc ? 'desc' : 'asc')
|
|
380
|
-
}
|
|
379
|
+
appendCustomerListSortParams(params, sorting)
|
|
381
380
|
const advancedParams = serializeTree(advancedFilterState)
|
|
382
381
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
383
382
|
params.set(key, val)
|
|
@@ -820,6 +819,7 @@ export default function CustomersCompaniesPage() {
|
|
|
820
819
|
.map<ColumnDef<CompanyRow>>((def) => ({
|
|
821
820
|
accessorKey: `cf_${def.key}`,
|
|
822
821
|
header: def.label || def.key,
|
|
822
|
+
enableSorting: true,
|
|
823
823
|
meta: {
|
|
824
824
|
columnChooserGroup: def.group?.title ?? 'Custom Fields',
|
|
825
825
|
filterGroup: def.group?.title ?? 'Custom Fields',
|
|
@@ -887,8 +887,9 @@ export default function CustomersCompaniesPage() {
|
|
|
887
887
|
onRowClick={(row) => router.push(`/backend/customers/companies-v2/${row.id}`)}
|
|
888
888
|
perspective={{ tableId: 'customers.companies.list' }}
|
|
889
889
|
sortable
|
|
890
|
+
manualSorting
|
|
890
891
|
sorting={sorting}
|
|
891
|
-
onSortingChange={
|
|
892
|
+
onSortingChange={handleSortingChange}
|
|
892
893
|
bulkActions={[
|
|
893
894
|
{
|
|
894
895
|
id: 'delete',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SortingState } from '@tanstack/react-table'
|
|
2
|
+
|
|
3
|
+
const CUSTOMER_LIST_SORT_FIELDS: Record<string, string> = {
|
|
4
|
+
name: 'name',
|
|
5
|
+
email: 'primaryEmail',
|
|
6
|
+
status: 'status',
|
|
7
|
+
lifecycleStage: 'lifecycleStage',
|
|
8
|
+
source: 'source',
|
|
9
|
+
nextInteractionAt: 'nextInteractionAt',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveCustomerListSortField(columnId: string): string | null {
|
|
13
|
+
const normalized = columnId.trim()
|
|
14
|
+
if (!normalized) return null
|
|
15
|
+
if (normalized.startsWith('cf:')) return normalized
|
|
16
|
+
if (normalized.startsWith('cf_')) return `cf:${normalized.slice(3)}`
|
|
17
|
+
return CUSTOMER_LIST_SORT_FIELDS[normalized] ?? null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function appendCustomerListSortParams(params: URLSearchParams, sorting: SortingState): void {
|
|
21
|
+
const activeSort = sorting[0]
|
|
22
|
+
if (!activeSort) return
|
|
23
|
+
const sortField = resolveCustomerListSortField(activeSort.id)
|
|
24
|
+
if (!sortField) return
|
|
25
|
+
params.set('sortField', sortField)
|
|
26
|
+
params.set('sortDir', activeSort.desc ? 'desc' : 'asc')
|
|
27
|
+
}
|
|
@@ -5,7 +5,7 @@ import Link from 'next/link'
|
|
|
5
5
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
6
6
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
7
|
import { DataTable, type DataTableExportFormat, withDataTableNamespaces } from '@open-mercato/ui/backend/DataTable'
|
|
8
|
-
import type { ColumnDef } from '@tanstack/react-table'
|
|
8
|
+
import type { ColumnDef, SortingState } from '@tanstack/react-table'
|
|
9
9
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
10
10
|
import { RowActions } from '@open-mercato/ui/backend/RowActions'
|
|
11
11
|
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
mapAssignableStaffToFilterOptions,
|
|
54
54
|
} from '../../../components/detail/assignableStaff'
|
|
55
55
|
import { CollectionPreviewCell, normalizeCollectionLabels } from '../../../components/list/CollectionPreviewCell'
|
|
56
|
+
import { appendCustomerListSortParams } from '../listSorting'
|
|
56
57
|
|
|
57
58
|
type DictionaryOptionWithTone = AdvancedFilterOption & FilterOption
|
|
58
59
|
|
|
@@ -199,7 +200,7 @@ export default function CustomersPeoplePage() {
|
|
|
199
200
|
const [rows, setRows] = React.useState<PersonRow[]>([])
|
|
200
201
|
const [page, setPage] = React.useState(1)
|
|
201
202
|
const [pageSize, setPageSize] = React.useState(20)
|
|
202
|
-
const [sorting, setSorting] = React.useState<
|
|
203
|
+
const [sorting, setSorting] = React.useState<SortingState>([])
|
|
203
204
|
const [total, setTotal] = React.useState(0)
|
|
204
205
|
const [totalPages, setTotalPages] = React.useState(1)
|
|
205
206
|
const [search, setSearch] = React.useState('')
|
|
@@ -256,6 +257,10 @@ export default function CustomersPeoplePage() {
|
|
|
256
257
|
setPageSize(newSize)
|
|
257
258
|
setPage(1)
|
|
258
259
|
}, [])
|
|
260
|
+
const handleSortingChange = React.useCallback((nextSorting: SortingState) => {
|
|
261
|
+
setSorting(nextSorting)
|
|
262
|
+
setPage(1)
|
|
263
|
+
}, [])
|
|
259
264
|
|
|
260
265
|
const bulkMutationContextId = 'customers-people-list:bulk-delete'
|
|
261
266
|
const { runMutation: runBulkMutation, retryLastMutation: retryBulkMutation } = useGuardedMutation<{
|
|
@@ -361,10 +366,7 @@ export default function CustomersPeoplePage() {
|
|
|
361
366
|
const params = new URLSearchParams()
|
|
362
367
|
params.set('page', String(page))
|
|
363
368
|
params.set('pageSize', String(pageSize))
|
|
364
|
-
|
|
365
|
-
params.set('sort', sorting[0].id)
|
|
366
|
-
params.set('order', sorting[0].desc ? 'desc' : 'asc')
|
|
367
|
-
}
|
|
369
|
+
appendCustomerListSortParams(params, sorting)
|
|
368
370
|
if (search.trim()) params.set('search', search.trim())
|
|
369
371
|
const advancedParams = serializeTree(advancedFilterState)
|
|
370
372
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
@@ -386,10 +388,7 @@ export default function CustomersPeoplePage() {
|
|
|
386
388
|
const params = new URLSearchParams()
|
|
387
389
|
if (search.trim().length) params.set('search', search.trim())
|
|
388
390
|
if (page > 1) params.set('page', String(page))
|
|
389
|
-
|
|
390
|
-
params.set('sort', sorting[0].id)
|
|
391
|
-
params.set('order', sorting[0].desc ? 'desc' : 'asc')
|
|
392
|
-
}
|
|
391
|
+
appendCustomerListSortParams(params, sorting)
|
|
393
392
|
const advancedParams = serializeTree(advancedFilterState)
|
|
394
393
|
for (const [key, val] of Object.entries(advancedParams)) {
|
|
395
394
|
params.set(key, val)
|
|
@@ -847,6 +846,7 @@ export default function CustomersPeoplePage() {
|
|
|
847
846
|
.map<ColumnDef<PersonRow>>((def) => ({
|
|
848
847
|
accessorKey: `cf_${def.key}`,
|
|
849
848
|
header: def.label || def.key,
|
|
849
|
+
enableSorting: true,
|
|
850
850
|
meta: {
|
|
851
851
|
columnChooserGroup: def.group?.title ?? 'Custom Fields',
|
|
852
852
|
filterGroup: def.group?.title ?? 'Custom Fields',
|
|
@@ -914,8 +914,9 @@ export default function CustomersPeoplePage() {
|
|
|
914
914
|
perspective={{ tableId: 'customers.people.list' }}
|
|
915
915
|
onRowClick={(row) => router.push(`/backend/customers/people-v2/${row.id}`)}
|
|
916
916
|
sortable
|
|
917
|
+
manualSorting
|
|
917
918
|
sorting={sorting}
|
|
918
|
-
onSortingChange={
|
|
919
|
+
onSortingChange={handleSortingChange}
|
|
919
920
|
bulkActions={[
|
|
920
921
|
{
|
|
921
922
|
id: 'delete',
|
|
@@ -10,7 +10,7 @@ import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customF
|
|
|
10
10
|
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
11
11
|
import { updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
|
|
12
12
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
13
|
-
import { ActivitiesSection, NotesSection, type SectionAction, type TagOption } from '@open-mercato/ui/backend/detail'
|
|
13
|
+
import { ActivitiesSection, NotesSection, RecordNotFoundState, type SectionAction, type TagOption } from '@open-mercato/ui/backend/detail'
|
|
14
14
|
import { VersionHistoryAction } from '@open-mercato/ui/backend/version-history'
|
|
15
15
|
import { SendObjectMessageDialog } from '@open-mercato/ui/backend/messages'
|
|
16
16
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -83,6 +83,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
83
83
|
const router = useRouter()
|
|
84
84
|
const searchParams = useSearchParams()
|
|
85
85
|
const [initialValues, setInitialValues] = React.useState<Record<string, unknown> | null>(null)
|
|
86
|
+
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
86
87
|
const [tags, setTags] = React.useState<TagOption[]>([])
|
|
87
88
|
const [activeTab, setActiveTab] = React.useState<'details' | 'availability'>('details')
|
|
88
89
|
const [activeDetailTab, setActiveDetailTab] = React.useState<'notes' | 'activities'>('notes')
|
|
@@ -411,6 +412,7 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
411
412
|
|
|
412
413
|
React.useEffect(() => {
|
|
413
414
|
if (!resourceId || !resourceTypesLoaded) return
|
|
415
|
+
setIsNotFound(false)
|
|
414
416
|
let cancelled = false
|
|
415
417
|
async function loadResource() {
|
|
416
418
|
try {
|
|
@@ -421,7 +423,10 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
421
423
|
const record = await readApiResultOrThrow<ResourceResponse>(`/api/resources/resources?${params.toString()}`)
|
|
422
424
|
const resourceRaw = Array.isArray(record?.items) ? record.items[0] : null
|
|
423
425
|
const resource = resourceRaw ? normalizeResourceRecord(resourceRaw) : null
|
|
424
|
-
if (!resource)
|
|
426
|
+
if (!resource) {
|
|
427
|
+
if (!cancelled) setIsNotFound(true)
|
|
428
|
+
return
|
|
429
|
+
}
|
|
425
430
|
if (!cancelled) {
|
|
426
431
|
const customValues = extractCustomFieldEntries(resource)
|
|
427
432
|
setTags(Array.isArray(resource.tags) ? resource.tags : [])
|
|
@@ -479,6 +484,20 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
|
|
|
479
484
|
? initialValues.name.trim()
|
|
480
485
|
: t('resources.resources.detail.untitled', 'Unnamed resource')
|
|
481
486
|
|
|
487
|
+
if (isNotFound) {
|
|
488
|
+
return (
|
|
489
|
+
<Page>
|
|
490
|
+
<PageBody>
|
|
491
|
+
<RecordNotFoundState
|
|
492
|
+
label={t('resources.resources.form.errors.notFound', 'Resource not found.')}
|
|
493
|
+
backHref="/backend/resources/resources"
|
|
494
|
+
backLabel={t('resources.resources.detail.back', 'Back to resources')}
|
|
495
|
+
/>
|
|
496
|
+
</PageBody>
|
|
497
|
+
</Page>
|
|
498
|
+
)
|
|
499
|
+
}
|
|
500
|
+
|
|
482
501
|
return (
|
|
483
502
|
<Page>
|
|
484
503
|
<PageBody>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ErrorMessage,
|
|
12
12
|
InlineTextEditor,
|
|
13
13
|
LoadingMessage,
|
|
14
|
+
RecordNotFoundState,
|
|
14
15
|
TabEmptyState,
|
|
15
16
|
TagsSection,
|
|
16
17
|
type TagOption,
|
|
@@ -1876,6 +1877,7 @@ export default function SalesDocumentDetailPage({
|
|
|
1876
1877
|
const searchParams = useSearchParams()
|
|
1877
1878
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
1878
1879
|
const [loading, setLoading] = React.useState(true)
|
|
1880
|
+
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
1879
1881
|
const [record, setRecord] = React.useState<DocumentRecord | null>(null)
|
|
1880
1882
|
const [tags, setTags] = React.useState<TagOption[]>([])
|
|
1881
1883
|
const [kind, setKind] = React.useState<'order' | 'quote'>('quote')
|
|
@@ -2483,6 +2485,7 @@ export default function SalesDocumentDetailPage({
|
|
|
2483
2485
|
async function load() {
|
|
2484
2486
|
setLoading(true)
|
|
2485
2487
|
setError(null)
|
|
2488
|
+
setIsNotFound(false)
|
|
2486
2489
|
const requestedKind = searchParams.get('kind')
|
|
2487
2490
|
const preferredKind = requestedKind === 'order' ? 'order' : requestedKind === 'quote' ? 'quote' : initialKind ?? null
|
|
2488
2491
|
const kindsToTry: Array<'order' | 'quote'> = preferredKind
|
|
@@ -2505,7 +2508,11 @@ export default function SalesDocumentDetailPage({
|
|
|
2505
2508
|
}
|
|
2506
2509
|
if (!cancelled) {
|
|
2507
2510
|
setLoading(false)
|
|
2508
|
-
|
|
2511
|
+
if (lastError) {
|
|
2512
|
+
setError(lastError)
|
|
2513
|
+
} else {
|
|
2514
|
+
setIsNotFound(true)
|
|
2515
|
+
}
|
|
2509
2516
|
}
|
|
2510
2517
|
}
|
|
2511
2518
|
load().catch((err) => {
|
|
@@ -4435,6 +4442,26 @@ export default function SalesDocumentDetailPage({
|
|
|
4435
4442
|
)
|
|
4436
4443
|
}
|
|
4437
4444
|
|
|
4445
|
+
if (isNotFound) {
|
|
4446
|
+
const backHref = (searchParams.get('kind') === 'order' || initialKind === 'order')
|
|
4447
|
+
? '/backend/sales/orders'
|
|
4448
|
+
: '/backend/sales/quotes'
|
|
4449
|
+
const backLabel = (searchParams.get('kind') === 'order' || initialKind === 'order')
|
|
4450
|
+
? t('sales.documents.detail.backToOrders', 'Back to orders')
|
|
4451
|
+
: t('sales.documents.detail.backToQuotes', 'Back to quotes')
|
|
4452
|
+
return (
|
|
4453
|
+
<Page>
|
|
4454
|
+
<PageBody>
|
|
4455
|
+
<RecordNotFoundState
|
|
4456
|
+
label={t('sales.documents.detail.notFound', 'Document not found.')}
|
|
4457
|
+
backHref={backHref}
|
|
4458
|
+
backLabel={backLabel}
|
|
4459
|
+
/>
|
|
4460
|
+
</PageBody>
|
|
4461
|
+
</Page>
|
|
4462
|
+
)
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4438
4465
|
if (error) {
|
|
4439
4466
|
return (
|
|
4440
4467
|
<Page>
|
|
@@ -552,6 +552,8 @@
|
|
|
552
552
|
"sales.documents.detail.adjustments.updated": "Anpassung aktualisiert.",
|
|
553
553
|
"sales.documents.detail.back": "Zurück zu Sales",
|
|
554
554
|
"sales.documents.detail.backToCreate": "Neues Dokument erstellen",
|
|
555
|
+
"sales.documents.detail.backToOrders": "Zurück zu Bestellungen",
|
|
556
|
+
"sales.documents.detail.backToQuotes": "Zurück zu Angeboten",
|
|
555
557
|
"sales.documents.detail.billing": "Rechnungsadresse",
|
|
556
558
|
"sales.documents.detail.channel": "Kanal",
|
|
557
559
|
"sales.documents.detail.channelInvalid": "Ausgewählter Kanal wurde nicht gefunden.",
|
|
@@ -679,6 +681,7 @@
|
|
|
679
681
|
"sales.documents.detail.items.variant": "Variante",
|
|
680
682
|
"sales.documents.detail.items.variantSearch": "Variante suchen",
|
|
681
683
|
"sales.documents.detail.loading": "Dokument wird geladen…",
|
|
684
|
+
"sales.documents.detail.notFound": "Dokument nicht gefunden.",
|
|
682
685
|
"sales.documents.detail.number": "Dokumentnummer",
|
|
683
686
|
"sales.documents.detail.numberEditForbidden": "Dokumentennummer kann nicht bearbeitet werden.",
|
|
684
687
|
"sales.documents.detail.numberEmpty": "Noch keine Nummer",
|
|
@@ -552,6 +552,8 @@
|
|
|
552
552
|
"sales.documents.detail.adjustments.updated": "Adjustment updated.",
|
|
553
553
|
"sales.documents.detail.back": "Back to Sales",
|
|
554
554
|
"sales.documents.detail.backToCreate": "Create a new document",
|
|
555
|
+
"sales.documents.detail.backToOrders": "Back to orders",
|
|
556
|
+
"sales.documents.detail.backToQuotes": "Back to quotes",
|
|
555
557
|
"sales.documents.detail.billing": "Billing address",
|
|
556
558
|
"sales.documents.detail.channel": "Channel",
|
|
557
559
|
"sales.documents.detail.channelInvalid": "Selected channel could not be found.",
|
|
@@ -679,6 +681,7 @@
|
|
|
679
681
|
"sales.documents.detail.items.variant": "Variant",
|
|
680
682
|
"sales.documents.detail.items.variantSearch": "Search variant",
|
|
681
683
|
"sales.documents.detail.loading": "Loading document…",
|
|
684
|
+
"sales.documents.detail.notFound": "Document not found.",
|
|
682
685
|
"sales.documents.detail.number": "Document number",
|
|
683
686
|
"sales.documents.detail.numberEditForbidden": "Document number cannot be edited.",
|
|
684
687
|
"sales.documents.detail.numberEmpty": "No number yet",
|
|
@@ -552,6 +552,8 @@
|
|
|
552
552
|
"sales.documents.detail.adjustments.updated": "Ajuste actualizado.",
|
|
553
553
|
"sales.documents.detail.back": "Volver a Ventas",
|
|
554
554
|
"sales.documents.detail.backToCreate": "Crear un nuevo documento",
|
|
555
|
+
"sales.documents.detail.backToOrders": "Volver a pedidos",
|
|
556
|
+
"sales.documents.detail.backToQuotes": "Volver a cotizaciones",
|
|
555
557
|
"sales.documents.detail.billing": "Dirección de facturación",
|
|
556
558
|
"sales.documents.detail.channel": "Canal",
|
|
557
559
|
"sales.documents.detail.channelInvalid": "No se pudo encontrar el canal seleccionado.",
|
|
@@ -679,6 +681,7 @@
|
|
|
679
681
|
"sales.documents.detail.items.variant": "Variante",
|
|
680
682
|
"sales.documents.detail.items.variantSearch": "Buscar variante",
|
|
681
683
|
"sales.documents.detail.loading": "Cargando documento…",
|
|
684
|
+
"sales.documents.detail.notFound": "Documento no encontrado.",
|
|
682
685
|
"sales.documents.detail.number": "Número de documento",
|
|
683
686
|
"sales.documents.detail.numberEditForbidden": "El número de documento no se puede editar.",
|
|
684
687
|
"sales.documents.detail.numberEmpty": "Sin número aún",
|
|
@@ -552,6 +552,8 @@
|
|
|
552
552
|
"sales.documents.detail.adjustments.updated": "Korekta zaktualizowana.",
|
|
553
553
|
"sales.documents.detail.back": "Wróć do sprzedaży",
|
|
554
554
|
"sales.documents.detail.backToCreate": "Utwórz nowy dokument",
|
|
555
|
+
"sales.documents.detail.backToOrders": "Powrót do zamówień",
|
|
556
|
+
"sales.documents.detail.backToQuotes": "Powrót do ofert",
|
|
555
557
|
"sales.documents.detail.billing": "Adres rozliczeniowy",
|
|
556
558
|
"sales.documents.detail.channel": "Kanał",
|
|
557
559
|
"sales.documents.detail.channelInvalid": "Nie znaleziono wybranego kanału.",
|
|
@@ -679,6 +681,7 @@
|
|
|
679
681
|
"sales.documents.detail.items.variant": "Wariant",
|
|
680
682
|
"sales.documents.detail.items.variantSearch": "Szukaj wariantu",
|
|
681
683
|
"sales.documents.detail.loading": "Ładowanie dokumentu…",
|
|
684
|
+
"sales.documents.detail.notFound": "Nie znaleziono dokumentu.",
|
|
682
685
|
"sales.documents.detail.number": "Numer dokumentu",
|
|
683
686
|
"sales.documents.detail.numberEditForbidden": "Numer dokumentu nie może być edytowany.",
|
|
684
687
|
"sales.documents.detail.numberEmpty": "Brak numeru",
|