@open-mercato/core 0.9.1-develop.7498.1.08d233a6cc → 0.9.1-develop.7513.1.ae2715553f
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/audit_logs/components/AuditLogsActions.js +1 -1
- package/dist/modules/audit_logs/components/AuditLogsActions.js.map +2 -2
- package/dist/modules/auth/frontend/reset.js +7 -3
- package/dist/modules/auth/frontend/reset.js.map +2 -2
- package/dist/modules/business_rules/api/rules/route.js +12 -1
- package/dist/modules/business_rules/api/rules/route.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +17 -2
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
- package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
- package/dist/modules/catalog/backend/catalog/products/create/page.js +1 -0
- package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
- package/dist/modules/catalog/components/products/productFormUtils.js +9 -0
- package/dist/modules/catalog/components/products/productFormUtils.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js +32 -32
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +22 -22
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +34 -34
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/ecommerce/search.js +1 -2
- package/dist/modules/ecommerce/search.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js +10 -7
- package/dist/modules/staff/backend/staff/time-tracking/entries/page.js.map +2 -2
- package/dist/modules/wms/api/interceptors.js +3 -0
- package/dist/modules/wms/api/interceptors.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +16 -16
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/events.js +1 -0
- package/dist/modules/wms/events.js.map +2 -2
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js +9 -0
- package/dist/modules/wms/lib/catalogInventoryProfileIntent.js.map +2 -2
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js +11 -3
- package/dist/modules/wms/widgets/injection/catalog-inventory-profile/widget.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/audit_logs/components/AuditLogsActions.tsx +1 -1
- package/src/modules/auth/frontend/reset.tsx +8 -3
- package/src/modules/business_rules/api/rules/route.ts +11 -0
- package/src/modules/business_rules/i18n/de.json +1 -0
- package/src/modules/business_rules/i18n/en.json +1 -0
- package/src/modules/business_rules/i18n/es.json +1 -0
- package/src/modules/business_rules/i18n/ko.json +1 -0
- package/src/modules/business_rules/i18n/pl.json +1 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +20 -1
- package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +2 -1
- package/src/modules/catalog/backend/catalog/products/create/page.tsx +1 -0
- package/src/modules/catalog/components/products/productFormUtils.ts +9 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +32 -32
- package/src/modules/customers/backend/customers/deals/page.tsx +22 -22
- package/src/modules/customers/backend/customers/people/page.tsx +34 -34
- package/src/modules/customers/i18n/de.json +12 -0
- package/src/modules/customers/i18n/en.json +12 -0
- package/src/modules/customers/i18n/es.json +12 -0
- package/src/modules/customers/i18n/ko.json +12 -0
- package/src/modules/customers/i18n/pl.json +12 -0
- package/src/modules/ecommerce/i18n/de.json +2 -1
- package/src/modules/ecommerce/i18n/en.json +2 -1
- package/src/modules/ecommerce/i18n/es.json +2 -1
- package/src/modules/ecommerce/i18n/ko.json +2 -1
- package/src/modules/ecommerce/i18n/pl.json +2 -1
- package/src/modules/ecommerce/search.ts +0 -1
- package/src/modules/staff/backend/staff/time-tracking/entries/page.tsx +20 -14
- package/src/modules/wms/api/interceptors.ts +9 -0
- package/src/modules/wms/commands/configuration.ts +28 -16
- package/src/modules/wms/events.ts +1 -0
- package/src/modules/wms/lib/catalogInventoryProfileIntent.ts +9 -0
- package/src/modules/wms/widgets/injection/catalog-inventory-profile/widget.ts +13 -4
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "Path prefix may contain only lowercase letters, digits, dashes and slashes, for example /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "Slug may contain only lowercase letters, digits and single dashes between them.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Enter a valid http(s) URL or a path starting with /.",
|
|
340
|
-
"notifications.categories.ecommerce": "Ecommerce"
|
|
340
|
+
"notifications.categories.ecommerce": "Ecommerce",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Store"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "El prefijo de ruta solo puede contener letras minúsculas, dígitos, guiones y barras, por ejemplo /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "El slug solo puede contener letras minúsculas, dígitos y guiones simples entre ellos.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Introduce una URL http(s) válida o una ruta que empiece por /.",
|
|
340
|
-
"notifications.categories.ecommerce": "Comercio electrónico"
|
|
340
|
+
"notifications.categories.ecommerce": "Comercio electrónico",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Tienda"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "경로 접두사에는 영문 소문자, 숫자, 하이픈, 슬래시만 사용할 수 있습니다(예: /shop).",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "슬러그에는 영문 소문자, 숫자와 그 사이의 단일 하이픈만 사용할 수 있습니다.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "유효한 http(s) URL 또는 /로 시작하는 경로를 입력하세요.",
|
|
340
|
-
"notifications.categories.ecommerce": "이커머스"
|
|
340
|
+
"notifications.categories.ecommerce": "이커머스",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "스토어"
|
|
341
342
|
}
|
|
@@ -337,5 +337,6 @@
|
|
|
337
337
|
"ecommerce.validation.pathPrefixInvalid": "Prefiks ścieżki może zawierać tylko małe litery, cyfry, myślniki i ukośniki, np. /shop.",
|
|
338
338
|
"ecommerce.validation.slugInvalid": "Slug może zawierać tylko małe litery, cyfry i pojedyncze myślniki między nimi.",
|
|
339
339
|
"ecommerce.validation.urlInvalid": "Podaj prawidłowy adres URL http(s) lub ścieżkę zaczynającą się od /.",
|
|
340
|
-
"notifications.categories.ecommerce": "E-commerce"
|
|
340
|
+
"notifications.categories.ecommerce": "E-commerce",
|
|
341
|
+
"search.entityType.ecommerce.ecommerce_store": "Sklep"
|
|
341
342
|
}
|
|
@@ -353,7 +353,7 @@ export default function TimeTrackingEntriesPage() {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
const [selfRes, projectsRes, tagsRes, peopleRes] = await Promise.all([
|
|
356
|
-
safeCall<{ member?: { id?: string } | null }>('/api/staff/team-members/self'),
|
|
356
|
+
safeCall<{ member?: { id?: string; displayName?: string } | null }>('/api/staff/team-members/self'),
|
|
357
357
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/time-projects?page=1&pageSize=100'),
|
|
358
358
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/timesheets/tags?page=1&pageSize=100'),
|
|
359
359
|
safeCall<{ items?: Array<Record<string, unknown>> }>('/api/staff/team-members?page=1&pageSize=100'),
|
|
@@ -399,19 +399,25 @@ export default function TimeTrackingEntriesPage() {
|
|
|
399
399
|
)
|
|
400
400
|
|
|
401
401
|
const people = peopleRes?.ok && Array.isArray(peopleRes.result?.items) ? peopleRes.result.items : []
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
402
|
+
const personOptions = people
|
|
403
|
+
.map((row: Record<string, unknown>) => {
|
|
404
|
+
const id = typeof row.id === 'string' ? row.id : null
|
|
405
|
+
const label = typeof row.display_name === 'string'
|
|
406
|
+
? row.display_name
|
|
407
|
+
: typeof row.displayName === 'string'
|
|
408
|
+
? row.displayName
|
|
409
|
+
: null
|
|
410
|
+
return id && label ? { value: id, label } : null
|
|
411
|
+
})
|
|
412
|
+
.filter((option): option is { value: string; label: string } => option !== null)
|
|
413
|
+
// The team list is closed to a caller without team-member access, yet the
|
|
414
|
+
// self filter is seeded for everyone — name it from the self lookup so its
|
|
415
|
+
// chip never falls back to the raw member id.
|
|
416
|
+
const selfName = selfRes?.ok ? selfRes.result?.member?.displayName : undefined
|
|
417
|
+
if (selfId && selfName && !personOptions.some((option) => option.value === selfId)) {
|
|
418
|
+
personOptions.push({ value: selfId, label: selfName })
|
|
419
|
+
}
|
|
420
|
+
setPeopleOptions(personOptions)
|
|
415
421
|
})()
|
|
416
422
|
return () => { cancelled = true }
|
|
417
423
|
}, [scopeVersion])
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ApiInterceptor } from '@open-mercato/shared/lib/crud/api-interceptor'
|
|
2
|
+
import { E } from '#generated/entities.ids.generated'
|
|
2
3
|
import {
|
|
3
4
|
decodeCatalogInventoryProfileIntent,
|
|
4
5
|
WMS_CATALOG_PROFILE_HEADER,
|
|
@@ -35,6 +36,12 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
35
36
|
if (!rawHeader) return { ok: true }
|
|
36
37
|
|
|
37
38
|
const intent = decodeCatalogInventoryProfileIntent(rawHeader)
|
|
39
|
+
// CrudForm scopes `onBeforeSave` request headers around the whole
|
|
40
|
+
// `onSubmit` callback, so a page whose submit handler also creates
|
|
41
|
+
// other records (e.g. the product-create wizard implicitly creating
|
|
42
|
+
// its default variant) can leak this header onto those unrelated
|
|
43
|
+
// requests. Only honor an intent the product form itself computed.
|
|
44
|
+
if (intent?.sourceEntityId !== E.catalog.catalog_product) return { ok: true }
|
|
38
45
|
return {
|
|
39
46
|
ok: true,
|
|
40
47
|
metadata: { intent, target: 'product' },
|
|
@@ -74,6 +81,8 @@ export const interceptors: ApiInterceptor[] = [
|
|
|
74
81
|
if (!rawHeader) return { ok: true }
|
|
75
82
|
|
|
76
83
|
const intent = decodeCatalogInventoryProfileIntent(rawHeader)
|
|
84
|
+
// See the product interceptor's `before` for why this check exists.
|
|
85
|
+
if (intent?.sourceEntityId !== E.catalog.catalog_product_variant) return { ok: true }
|
|
77
86
|
return {
|
|
78
87
|
ok: true,
|
|
79
88
|
metadata: { intent, target: 'variant' },
|
|
@@ -1532,6 +1532,28 @@ function restoreInventoryProfileFromSnapshot(em: EntityManager, before: ProductI
|
|
|
1532
1532
|
record.deletedAt = null
|
|
1533
1533
|
}
|
|
1534
1534
|
|
|
1535
|
+
type InventoryProfileEventSource = {
|
|
1536
|
+
id: string
|
|
1537
|
+
catalogProductId: string
|
|
1538
|
+
catalogVariantId?: string | null
|
|
1539
|
+
tenantId: string
|
|
1540
|
+
organizationId: string
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function emitInventoryProfileEvent(
|
|
1544
|
+
eventId: 'wms.inventory_profile.created' | 'wms.inventory_profile.updated' | 'wms.inventory_profile.deleted',
|
|
1545
|
+
profile: InventoryProfileEventSource,
|
|
1546
|
+
): void {
|
|
1547
|
+
void emitWmsEvent(eventId, {
|
|
1548
|
+
id: profile.id,
|
|
1549
|
+
profileId: profile.id,
|
|
1550
|
+
catalogProductId: profile.catalogProductId,
|
|
1551
|
+
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1552
|
+
tenantId: profile.tenantId,
|
|
1553
|
+
organizationId: profile.organizationId,
|
|
1554
|
+
}).catch(() => undefined)
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1535
1557
|
const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfileCreateInput, { profileId: string }> = {
|
|
1536
1558
|
id: 'wms.inventoryProfiles.create',
|
|
1537
1559
|
async execute(rawInput, ctx) {
|
|
@@ -1555,14 +1577,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1555
1577
|
metadata: toJsonValue(parsed.metadata),
|
|
1556
1578
|
})
|
|
1557
1579
|
await em.persist(profile).flush()
|
|
1558
|
-
|
|
1559
|
-
id: profile.id,
|
|
1560
|
-
profileId: profile.id,
|
|
1561
|
-
catalogProductId: profile.catalogProductId,
|
|
1562
|
-
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1563
|
-
tenantId: profile.tenantId,
|
|
1564
|
-
organizationId: profile.organizationId,
|
|
1565
|
-
}).catch(() => undefined)
|
|
1580
|
+
emitInventoryProfileEvent('wms.inventory_profile.created', profile)
|
|
1566
1581
|
return { profileId: profile.id }
|
|
1567
1582
|
},
|
|
1568
1583
|
captureAfter: async (_input, result, ctx) => {
|
|
@@ -1591,6 +1606,7 @@ const createProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1591
1606
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
1592
1607
|
record.deletedAt = new Date()
|
|
1593
1608
|
await em.flush()
|
|
1609
|
+
emitInventoryProfileEvent('wms.inventory_profile.deleted', record)
|
|
1594
1610
|
},
|
|
1595
1611
|
}
|
|
1596
1612
|
|
|
@@ -1629,14 +1645,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1629
1645
|
if (parsed.safetyStock !== undefined) profile.safetyStock = toNumericString(parsed.safetyStock)
|
|
1630
1646
|
if (parsed.metadata !== undefined) profile.metadata = toJsonValue(parsed.metadata)
|
|
1631
1647
|
await em.flush()
|
|
1632
|
-
|
|
1633
|
-
id: profile.id,
|
|
1634
|
-
profileId: profile.id,
|
|
1635
|
-
catalogProductId: profile.catalogProductId,
|
|
1636
|
-
catalogVariantId: profile.catalogVariantId ?? null,
|
|
1637
|
-
tenantId: profile.tenantId,
|
|
1638
|
-
organizationId: profile.organizationId,
|
|
1639
|
-
}).catch(() => undefined)
|
|
1648
|
+
emitInventoryProfileEvent('wms.inventory_profile.updated', profile)
|
|
1640
1649
|
return { profileId: profile.id }
|
|
1641
1650
|
},
|
|
1642
1651
|
captureAfter: async (_input, result, ctx) => {
|
|
@@ -1686,6 +1695,7 @@ const updateProductInventoryProfileCommand: CommandHandler<ProductInventoryProfi
|
|
|
1686
1695
|
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1687
1696
|
}
|
|
1688
1697
|
await em.flush()
|
|
1698
|
+
emitInventoryProfileEvent('wms.inventory_profile.updated', before)
|
|
1689
1699
|
},
|
|
1690
1700
|
}
|
|
1691
1701
|
|
|
@@ -1703,6 +1713,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
|
|
|
1703
1713
|
const profile = await loadProfile(em, ctx, profileId)
|
|
1704
1714
|
profile.deletedAt = new Date()
|
|
1705
1715
|
await em.flush()
|
|
1716
|
+
emitInventoryProfileEvent('wms.inventory_profile.deleted', profile)
|
|
1706
1717
|
return { profileId: profile.id }
|
|
1707
1718
|
},
|
|
1708
1719
|
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
@@ -1747,6 +1758,7 @@ const deleteProductInventoryProfileCommand: CommandHandler<{ id?: string }, { pr
|
|
|
1747
1758
|
restoreInventoryProfileFromSnapshot(em, before, record)
|
|
1748
1759
|
}
|
|
1749
1760
|
await em.flush()
|
|
1761
|
+
emitInventoryProfileEvent('wms.inventory_profile.created', before)
|
|
1750
1762
|
},
|
|
1751
1763
|
}
|
|
1752
1764
|
|
|
@@ -9,6 +9,7 @@ const events = [
|
|
|
9
9
|
{ id: 'wms.location.updated', label: 'Warehouse Location Updated', entity: 'location', category: 'crud' },
|
|
10
10
|
{ id: 'wms.inventory_profile.created', label: 'Inventory Profile Created', entity: 'inventory_profile', category: 'crud' },
|
|
11
11
|
{ id: 'wms.inventory_profile.updated', label: 'Inventory Profile Updated', entity: 'inventory_profile', category: 'crud' },
|
|
12
|
+
{ id: 'wms.inventory_profile.deleted', label: 'Inventory Profile Deleted', entity: 'inventory_profile', category: 'crud' },
|
|
12
13
|
{ id: 'wms.inventory_balance.created', label: 'Inventory Balance Created', entity: 'inventory_balance', category: 'crud' },
|
|
13
14
|
{ id: 'wms.inventory_balance.updated', label: 'Inventory Balance Updated', entity: 'inventory_balance', category: 'crud' },
|
|
14
15
|
{ id: 'wms.inventory_balance.deleted', label: 'Inventory Balance Deleted', entity: 'inventory_balance', category: 'crud' },
|
|
@@ -3,6 +3,15 @@ import { z } from 'zod'
|
|
|
3
3
|
const inventoryStrategySchema = z.enum(['fifo', 'lifo', 'fefo'])
|
|
4
4
|
|
|
5
5
|
export const catalogInventoryProfileIntentSchema = z.object({
|
|
6
|
+
// Identifies which host CrudForm (product vs. variant) computed this intent.
|
|
7
|
+
// CrudForm scopes `onBeforeSave` request headers around the whole `onSubmit`
|
|
8
|
+
// callback, not around a single HTTP call, so a page whose submit handler
|
|
9
|
+
// creates secondary records (e.g. the product-create wizard implicitly
|
|
10
|
+
// creating its default variant) can leak this header onto those unrelated
|
|
11
|
+
// requests. Each interceptor only acts when this matches its own target
|
|
12
|
+
// entity, so a leaked intent is ignored rather than creating a profile the
|
|
13
|
+
// user never asked for on that other entity.
|
|
14
|
+
sourceEntityId: z.string(),
|
|
6
15
|
manageInventory: z.boolean(),
|
|
7
16
|
defaultUom: z.string().trim().max(32).nullable().optional(),
|
|
8
17
|
defaultStrategy: inventoryStrategySchema.optional(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
|
|
1
|
+
import type { InjectionContext, InjectionFieldWidget } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
2
|
import {
|
|
3
3
|
encodeCatalogInventoryProfileIntent,
|
|
4
4
|
WMS_CATALOG_PROFILE_HEADER,
|
|
@@ -29,8 +29,9 @@ function readNumber(value: unknown): number | undefined {
|
|
|
29
29
|
return undefined
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
|
|
32
|
+
function buildIntent(data: RecordLike, sourceEntityId: string): CatalogInventoryProfileIntent {
|
|
33
33
|
return {
|
|
34
|
+
sourceEntityId,
|
|
34
35
|
manageInventory: readBoolean(data['wms.manageInventory']),
|
|
35
36
|
defaultUom: readString(data['wms.defaultUom']).trim() || null,
|
|
36
37
|
defaultStrategy: (() => {
|
|
@@ -47,6 +48,10 @@ function buildIntent(data: RecordLike): CatalogInventoryProfileIntent {
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function hasLoadedInventoryProfile(data: RecordLike): boolean {
|
|
52
|
+
return Boolean(asRecord(asRecord(data._wms)?.inventoryProfile))
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
function buildDisplayState(data: RecordLike): RecordLike {
|
|
51
56
|
const wms = asRecord(data._wms)
|
|
52
57
|
const inventoryProfile = asRecord(wms?.inventoryProfile)
|
|
@@ -170,12 +175,16 @@ const widget: InjectionFieldWidget = {
|
|
|
170
175
|
if (!source) return data
|
|
171
176
|
return buildDisplayState(source) as typeof data
|
|
172
177
|
},
|
|
173
|
-
async onBeforeSave(data) {
|
|
178
|
+
async onBeforeSave(data, context: InjectionContext) {
|
|
174
179
|
const source = asRecord(data)
|
|
175
180
|
if (!source) return { ok: true }
|
|
176
181
|
|
|
177
|
-
const
|
|
182
|
+
const sourceEntityId = readString(context?.entityId)
|
|
183
|
+
const intent = buildIntent(source, sourceEntityId)
|
|
178
184
|
if (!intent.manageInventory) {
|
|
185
|
+
if (source['wms.manageInventory'] !== false || !hasLoadedInventoryProfile(source)) {
|
|
186
|
+
return { ok: true }
|
|
187
|
+
}
|
|
179
188
|
return {
|
|
180
189
|
ok: true,
|
|
181
190
|
requestHeaders: {
|