@open-mercato/core 0.6.8-develop.7096.1.97319f09f6 → 0.6.8-develop.7100.1.fbf66fca35
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/api/users/route.js +13 -5
- package/dist/modules/auth/api/users/route.js.map +2 -2
- package/dist/modules/auth/lib/userIdFilter.js +16 -0
- package/dist/modules/auth/lib/userIdFilter.js.map +7 -0
- package/dist/modules/devices/acl.js +9 -1
- package/dist/modules/devices/acl.js.map +2 -2
- package/dist/modules/devices/backend/devices/[id]/page.js +4 -20
- package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/create/page.js +17 -1
- package/dist/modules/devices/backend/devices/create/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/page.js +13 -21
- package/dist/modules/devices/backend/devices/page.js.map +2 -2
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js +34 -0
- package/dist/modules/devices/backend/devices/useDeviceUserLabels.js.map +7 -0
- package/dist/modules/devices/backend/devices/userOptions.js +52 -0
- package/dist/modules/devices/backend/devices/userOptions.js.map +7 -0
- package/dist/modules/devices/setup.js +6 -2
- package/dist/modules/devices/setup.js.map +2 -2
- package/dist/modules/wms/api/warehouseSearch.js +21 -0
- package/dist/modules/wms/api/warehouseSearch.js.map +7 -0
- package/dist/modules/wms/api/warehouses/route.js +13 -9
- package/dist/modules/wms/api/warehouses/route.js.map +2 -2
- package/dist/modules/wms/commands/configuration.js +59 -5
- package/dist/modules/wms/commands/configuration.js.map +2 -2
- package/dist/modules/wms/commands/shared.js +4 -0
- package/dist/modules/wms/commands/shared.js.map +2 -2
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js +158 -0
- package/dist/modules/wms/components/backend/WarehouseEditDialog.js.map +7 -0
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js +29 -103
- package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
- package/dist/modules/wms/components/backend/warehouseFormOptions.js +66 -0
- package/dist/modules/wms/components/backend/warehouseFormOptions.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/api/users/route.ts +18 -5
- package/src/modules/auth/lib/userIdFilter.ts +31 -0
- package/src/modules/devices/AGENTS.md +12 -1
- package/src/modules/devices/acl.ts +9 -1
- package/src/modules/devices/backend/devices/[id]/page.tsx +5 -19
- package/src/modules/devices/backend/devices/create/page.tsx +17 -1
- package/src/modules/devices/backend/devices/page.tsx +20 -24
- package/src/modules/devices/backend/devices/useDeviceUserLabels.ts +45 -0
- package/src/modules/devices/backend/devices/userOptions.ts +99 -0
- package/src/modules/devices/i18n/de.json +3 -2
- package/src/modules/devices/i18n/en.json +3 -2
- package/src/modules/devices/i18n/es.json +3 -2
- package/src/modules/devices/i18n/ko.json +3 -2
- package/src/modules/devices/i18n/pl.json +3 -2
- package/src/modules/devices/setup.ts +6 -2
- package/src/modules/wms/api/warehouseSearch.ts +23 -0
- package/src/modules/wms/api/warehouses/route.ts +13 -9
- package/src/modules/wms/commands/configuration.ts +73 -7
- package/src/modules/wms/commands/shared.ts +9 -0
- package/src/modules/wms/components/backend/WarehouseEditDialog.tsx +216 -0
- package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +31 -139
- package/src/modules/wms/components/backend/warehouseFormOptions.ts +72 -0
- package/src/modules/wms/i18n/de.json +2 -0
- package/src/modules/wms/i18n/en.json +2 -0
- package/src/modules/wms/i18n/es.json +2 -0
- package/src/modules/wms/i18n/ko.json +2 -0
- package/src/modules/wms/i18n/pl.json +2 -0
|
@@ -31,16 +31,16 @@
|
|
|
31
31
|
import type { CommandHandler } from '@open-mercato/shared/lib/commands'
|
|
32
32
|
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
33
33
|
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
34
|
-
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
35
|
-
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
36
34
|
import {
|
|
37
35
|
emitCrudSideEffects,
|
|
38
36
|
emitCrudUndoSideEffects,
|
|
39
37
|
parseWithCustomFields,
|
|
40
38
|
setCustomFieldsIfAny,
|
|
41
39
|
} from '@open-mercato/shared/lib/commands/helpers'
|
|
40
|
+
import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
|
|
41
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
42
|
+
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
42
43
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
43
|
-
import { buildCustomFieldResetMap, loadCustomFieldSnapshot } from '@open-mercato/shared/lib/commands/customFieldSnapshots'
|
|
44
44
|
import { CrudHttpError, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
45
45
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
46
46
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
normalizeOptionalString,
|
|
86
86
|
requireId,
|
|
87
87
|
toNumericString,
|
|
88
|
+
warehouseCrudIndexer,
|
|
88
89
|
warehouseZoneCrudIndexer,
|
|
89
90
|
} from './shared'
|
|
90
91
|
import { emitWmsEvent } from '../events'
|
|
@@ -105,6 +106,22 @@ function toJsonValue(value: Record<string, unknown> | null | undefined): JsonVal
|
|
|
105
106
|
return (value ?? null) as JsonValue | null
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
async function persistWarehouseCustomFields(
|
|
110
|
+
ctx: CommandRuntimeContext,
|
|
111
|
+
warehouse: { id: string; organizationId: string; tenantId: string },
|
|
112
|
+
custom: Record<string, unknown>,
|
|
113
|
+
) {
|
|
114
|
+
if (!custom || Object.keys(custom).length === 0) return
|
|
115
|
+
await setCustomFieldsIfAny({
|
|
116
|
+
dataEngine: ctx.container.resolve('dataEngine'),
|
|
117
|
+
entityId: E.wms.warehouse,
|
|
118
|
+
recordId: warehouse.id,
|
|
119
|
+
organizationId: warehouse.organizationId,
|
|
120
|
+
tenantId: warehouse.tenantId,
|
|
121
|
+
values: custom,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
108
125
|
async function buildCrudLog(
|
|
109
126
|
ctx: CommandRuntimeContext,
|
|
110
127
|
input: { tenantId?: string | null; organizationId?: string | null; id?: string | null } | undefined,
|
|
@@ -184,6 +201,7 @@ type WarehouseSnapshot = {
|
|
|
184
201
|
metadata: JsonValue | null
|
|
185
202
|
createdAt: string
|
|
186
203
|
updatedAt: string
|
|
204
|
+
custom?: Record<string, unknown> | null
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
type PrimaryDemotionSnapshot = {
|
|
@@ -317,7 +335,16 @@ async function loadWarehouseSnapshot(
|
|
|
317
335
|
id: string,
|
|
318
336
|
): Promise<WarehouseSnapshot | null> {
|
|
319
337
|
const record = await findOneWithDecryption(em, Warehouse, { id }, undefined, resolveScope(ctx))
|
|
320
|
-
|
|
338
|
+
if (!record) return null
|
|
339
|
+
const snapshot = snapshotWarehouse(record)
|
|
340
|
+
const custom = await loadCustomFieldSnapshot(em, {
|
|
341
|
+
entityId: E.wms.warehouse,
|
|
342
|
+
recordId: record.id,
|
|
343
|
+
tenantId: record.tenantId,
|
|
344
|
+
organizationId: record.organizationId,
|
|
345
|
+
})
|
|
346
|
+
snapshot.custom = custom && Object.keys(custom).length ? custom : null
|
|
347
|
+
return snapshot
|
|
321
348
|
}
|
|
322
349
|
|
|
323
350
|
function snapshotWarehouseZone(record: WarehouseZone): WarehouseZoneSnapshot {
|
|
@@ -381,6 +408,30 @@ async function writeZoneCustomFields(
|
|
|
381
408
|
// `events` is deliberately omitted: the module already emits `wms.zone.*` itself,
|
|
382
409
|
// and adding an events config here would introduce a second, undeclared
|
|
383
410
|
// `wms.warehouse_zone.*` event id for the same write.
|
|
411
|
+
async function emitWarehouseCrudSideEffects(
|
|
412
|
+
ctx: CommandRuntimeContext,
|
|
413
|
+
action: 'created' | 'updated' | 'deleted',
|
|
414
|
+
warehouse: Warehouse,
|
|
415
|
+
origin: 'write' | 'undo' = 'write',
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
const options = {
|
|
418
|
+
dataEngine: ctx.container.resolve('dataEngine') as DataEngine,
|
|
419
|
+
action,
|
|
420
|
+
entity: warehouse,
|
|
421
|
+
identifiers: {
|
|
422
|
+
id: warehouse.id,
|
|
423
|
+
organizationId: warehouse.organizationId,
|
|
424
|
+
tenantId: warehouse.tenantId,
|
|
425
|
+
},
|
|
426
|
+
indexer: warehouseCrudIndexer,
|
|
427
|
+
}
|
|
428
|
+
if (origin === 'undo') {
|
|
429
|
+
await emitCrudUndoSideEffects(options)
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
await emitCrudSideEffects(options)
|
|
433
|
+
}
|
|
434
|
+
|
|
384
435
|
async function emitZoneCrudSideEffects(
|
|
385
436
|
ctx: CommandRuntimeContext,
|
|
386
437
|
action: 'created' | 'updated' | 'deleted',
|
|
@@ -714,11 +765,11 @@ async function resolveParentLocation(
|
|
|
714
765
|
|
|
715
766
|
const createWarehouseCommand: CommandHandler<
|
|
716
767
|
WarehouseCreateInput,
|
|
717
|
-
{ warehouseId: string; demotedPrimariesBefore?: PrimaryDemotionSnapshot[] }
|
|
768
|
+
{ warehouseId: string; updatedAt?: string | null; demotedPrimariesBefore?: PrimaryDemotionSnapshot[] }
|
|
718
769
|
> = {
|
|
719
770
|
id: 'wms.warehouses.create',
|
|
720
771
|
async execute(rawInput, ctx) {
|
|
721
|
-
const parsed = warehouseCreateSchema
|
|
772
|
+
const { parsed, custom } = parseWithCustomFields(warehouseCreateSchema, rawInput ?? {})
|
|
722
773
|
ensureTenantScope(ctx, parsed.tenantId)
|
|
723
774
|
ensureOrganizationScope(ctx, parsed.organizationId)
|
|
724
775
|
const em = resolveEm(ctx)
|
|
@@ -776,6 +827,8 @@ const createWarehouseCommand: CommandHandler<
|
|
|
776
827
|
}
|
|
777
828
|
throw err
|
|
778
829
|
}
|
|
830
|
+
await persistWarehouseCustomFields(ctx, warehouse, custom)
|
|
831
|
+
await emitWarehouseCrudSideEffects(ctx, 'created', warehouse)
|
|
779
832
|
void emitWmsEvent('wms.warehouse.created', {
|
|
780
833
|
id: warehouse.id,
|
|
781
834
|
warehouseId: warehouse.id,
|
|
@@ -784,6 +837,7 @@ const createWarehouseCommand: CommandHandler<
|
|
|
784
837
|
}).catch(() => undefined)
|
|
785
838
|
return {
|
|
786
839
|
warehouseId: warehouse.id,
|
|
840
|
+
updatedAt: isoOrNull(warehouse.updatedAt),
|
|
787
841
|
...(demotedPrimariesBefore.length > 0 ? { demotedPrimariesBefore } : {}),
|
|
788
842
|
}
|
|
789
843
|
},
|
|
@@ -823,6 +877,8 @@ const createWarehouseCommand: CommandHandler<
|
|
|
823
877
|
ensureOrganizationScope(ctx, record.organizationId)
|
|
824
878
|
record.deletedAt = new Date()
|
|
825
879
|
await em.flush()
|
|
880
|
+
await persistWarehouseCustomFields(ctx, after, buildCustomFieldResetMap(undefined, after.custom ?? undefined))
|
|
881
|
+
await emitWarehouseCrudSideEffects(ctx, 'deleted', record, 'undo')
|
|
826
882
|
if (payload.demotedPrimariesBefore?.length) {
|
|
827
883
|
await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore)
|
|
828
884
|
await em.flush()
|
|
@@ -842,7 +898,7 @@ const updateWarehouseCommand: CommandHandler<
|
|
|
842
898
|
return before ? { before } : {}
|
|
843
899
|
},
|
|
844
900
|
async execute(rawInput, ctx) {
|
|
845
|
-
const parsed = warehouseUpdateSchema
|
|
901
|
+
const { parsed, custom } = parseWithCustomFields(warehouseUpdateSchema, rawInput ?? {})
|
|
846
902
|
const em = resolveEm(ctx)
|
|
847
903
|
const warehouse = await loadWarehouse(em, ctx, parsed.id)
|
|
848
904
|
if (parsed.code !== undefined && parsed.code !== warehouse.code) {
|
|
@@ -902,6 +958,8 @@ const updateWarehouseCommand: CommandHandler<
|
|
|
902
958
|
}
|
|
903
959
|
throw err
|
|
904
960
|
}
|
|
961
|
+
await persistWarehouseCustomFields(ctx, warehouse, custom)
|
|
962
|
+
await emitWarehouseCrudSideEffects(ctx, 'updated', warehouse)
|
|
905
963
|
void emitWmsEvent('wms.warehouse.updated', {
|
|
906
964
|
id: warehouse.id,
|
|
907
965
|
warehouseId: warehouse.id,
|
|
@@ -982,6 +1040,12 @@ const updateWarehouseCommand: CommandHandler<
|
|
|
982
1040
|
record.deletedAt = null
|
|
983
1041
|
}
|
|
984
1042
|
await em.flush()
|
|
1043
|
+
await persistWarehouseCustomFields(
|
|
1044
|
+
ctx,
|
|
1045
|
+
before,
|
|
1046
|
+
buildCustomFieldResetMap(before.custom ?? undefined, payload.after?.custom ?? undefined),
|
|
1047
|
+
)
|
|
1048
|
+
await emitWarehouseCrudSideEffects(ctx, 'updated', record, 'undo')
|
|
985
1049
|
if (payload.demotedPrimariesBefore?.length) {
|
|
986
1050
|
await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore)
|
|
987
1051
|
await em.flush()
|
|
@@ -1003,6 +1067,7 @@ const deleteWarehouseCommand: CommandHandler<{ id?: string }, { warehouseId: str
|
|
|
1003
1067
|
const warehouse = await loadWarehouse(em, ctx, warehouseId)
|
|
1004
1068
|
warehouse.deletedAt = new Date()
|
|
1005
1069
|
await em.flush()
|
|
1070
|
+
await emitWarehouseCrudSideEffects(ctx, 'deleted', warehouse)
|
|
1006
1071
|
return { warehouseId: warehouse.id }
|
|
1007
1072
|
},
|
|
1008
1073
|
buildLog: async ({ input, result, ctx, snapshots }) => {
|
|
@@ -1057,6 +1122,7 @@ const deleteWarehouseCommand: CommandHandler<{ id?: string }, { warehouseId: str
|
|
|
1057
1122
|
record.metadata = before.metadata
|
|
1058
1123
|
}
|
|
1059
1124
|
await em.flush()
|
|
1125
|
+
await emitWarehouseCrudSideEffects(ctx, 'created', record, 'undo')
|
|
1060
1126
|
},
|
|
1061
1127
|
}
|
|
1062
1128
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
InventoryBalance,
|
|
8
8
|
InventoryMovement,
|
|
9
9
|
InventoryReservation,
|
|
10
|
+
Warehouse,
|
|
10
11
|
WarehouseZone,
|
|
11
12
|
} from '../data/entities'
|
|
12
13
|
|
|
@@ -56,6 +57,14 @@ export const warehouseZoneCrudIndexer: CrudIndexerConfig<WarehouseZone> = {
|
|
|
56
57
|
entityType: E.wms.warehouse_zone,
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
// Warehouse list/search goes through the hybrid query engine. Writing a custom
|
|
61
|
+
// field upserts `entity_indexes`; delete must tombstone that row. A leftover
|
|
62
|
+
// live index document makes `indexAnyRows` true and hides later warehouses that
|
|
63
|
+
// were never indexed (empty combobox on the same integration shard).
|
|
64
|
+
export const warehouseCrudIndexer: CrudIndexerConfig<Warehouse> = {
|
|
65
|
+
entityType: E.wms.warehouse,
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
export const inventoryBalanceCrudEvents: CrudEventsConfig<InventoryBalance> = {
|
|
60
69
|
module: 'wms',
|
|
61
70
|
entity: 'inventory_balance',
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'
|
|
6
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
7
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
8
|
+
import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
|
|
9
|
+
import { raiseCrudError } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
10
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
11
|
+
import { extractCustomFieldEntries } from '@open-mercato/shared/lib/crud/custom-fields-client'
|
|
12
|
+
import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
|
|
13
|
+
import { E } from '#generated/entities.ids.generated'
|
|
14
|
+
import { flashMutationError } from '../../lib/flashMutationError'
|
|
15
|
+
import {
|
|
16
|
+
countryOptionFromStored,
|
|
17
|
+
loadCountryOptions,
|
|
18
|
+
loadTimezoneOptions,
|
|
19
|
+
timezoneOptionFromStored,
|
|
20
|
+
} from './warehouseFormOptions'
|
|
21
|
+
|
|
22
|
+
export type WarehouseDialogRow = {
|
|
23
|
+
id: string
|
|
24
|
+
name?: string | null
|
|
25
|
+
code?: string | null
|
|
26
|
+
city?: string | null
|
|
27
|
+
country?: string | null
|
|
28
|
+
timezone?: string | null
|
|
29
|
+
is_active?: boolean | null
|
|
30
|
+
is_primary?: boolean | null
|
|
31
|
+
updated_at?: string | null
|
|
32
|
+
updatedAt?: string | null
|
|
33
|
+
customValues?: Record<string, unknown> | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type WarehouseFormValues = {
|
|
37
|
+
name: string
|
|
38
|
+
code: string
|
|
39
|
+
city?: string
|
|
40
|
+
country?: string
|
|
41
|
+
timezone?: string
|
|
42
|
+
isActive: boolean
|
|
43
|
+
isPrimary: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const warehouseFormSchema = z.object({
|
|
47
|
+
name: z.string().trim().min(1),
|
|
48
|
+
code: z.string().trim().min(1),
|
|
49
|
+
city: z.string().trim().optional(),
|
|
50
|
+
country: z.string().trim().optional(),
|
|
51
|
+
timezone: z.string().trim().optional(),
|
|
52
|
+
isActive: z.boolean().default(true),
|
|
53
|
+
isPrimary: z.boolean().default(false),
|
|
54
|
+
}).passthrough()
|
|
55
|
+
|
|
56
|
+
type WarehouseEditDialogProps = {
|
|
57
|
+
open: boolean
|
|
58
|
+
onOpenChange: (open: boolean) => void
|
|
59
|
+
mode: 'create' | 'edit'
|
|
60
|
+
row?: WarehouseDialogRow | null
|
|
61
|
+
onSaved?: (info: {
|
|
62
|
+
mode: 'create' | 'edit'
|
|
63
|
+
id?: string
|
|
64
|
+
values: WarehouseFormValues
|
|
65
|
+
updatedAt?: string | null
|
|
66
|
+
}) => void | Promise<void>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function WarehouseEditDialog({ open, onOpenChange, mode, row, onSaved }: WarehouseEditDialogProps) {
|
|
70
|
+
const t = useT()
|
|
71
|
+
const locale = useLocale()
|
|
72
|
+
const { runMutation } = useGuardedMutation<Record<string, unknown>>({ contextId: 'wms-config-warehouses' })
|
|
73
|
+
const [submitting, setSubmitting] = React.useState(false)
|
|
74
|
+
|
|
75
|
+
const countrySeedOptions = React.useMemo(() => {
|
|
76
|
+
if (mode !== 'edit' || !row?.country?.trim()) return undefined
|
|
77
|
+
return [countryOptionFromStored(row.country, locale)]
|
|
78
|
+
}, [locale, mode, row])
|
|
79
|
+
|
|
80
|
+
const timezoneSeedOptions = React.useMemo(() => {
|
|
81
|
+
if (mode !== 'edit' || !row?.timezone?.trim()) return undefined
|
|
82
|
+
return [timezoneOptionFromStored(row.timezone)]
|
|
83
|
+
}, [mode, row])
|
|
84
|
+
|
|
85
|
+
const fields = React.useMemo<CrudField[]>(() => [
|
|
86
|
+
{ id: 'name', type: 'text', label: t('wms.backend.config.warehouses.form.name', 'Name'), required: true },
|
|
87
|
+
{ id: 'code', type: 'text', label: t('wms.backend.config.warehouses.form.code', 'Code'), required: true },
|
|
88
|
+
{ id: 'city', type: 'text', label: t('wms.backend.config.warehouses.form.city', 'City') },
|
|
89
|
+
{
|
|
90
|
+
id: 'country',
|
|
91
|
+
type: 'combobox',
|
|
92
|
+
label: t('wms.backend.config.warehouses.form.country', 'Country'),
|
|
93
|
+
placeholder: t('wms.backend.config.warehouses.form.countryPlaceholder', 'Search country'),
|
|
94
|
+
loadOptions: (query) => loadCountryOptions(query, locale),
|
|
95
|
+
allowCustomValues: false,
|
|
96
|
+
seedOptions: countrySeedOptions,
|
|
97
|
+
resolveLabel: (value) => countryOptionFromStored(value, locale).label,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'timezone',
|
|
101
|
+
type: 'combobox',
|
|
102
|
+
label: t('wms.backend.config.warehouses.form.timezone', 'Timezone'),
|
|
103
|
+
placeholder: t('wms.backend.config.warehouses.form.timezonePlaceholder', 'Search timezone'),
|
|
104
|
+
loadOptions: loadTimezoneOptions,
|
|
105
|
+
allowCustomValues: false,
|
|
106
|
+
seedOptions: timezoneSeedOptions,
|
|
107
|
+
},
|
|
108
|
+
{ id: 'isPrimary', type: 'checkbox', label: t('wms.backend.config.warehouses.form.primary', 'Primary warehouse') },
|
|
109
|
+
{ id: 'isActive', type: 'checkbox', label: t('wms.backend.config.warehouses.form.active', 'Active') },
|
|
110
|
+
], [countrySeedOptions, locale, t, timezoneSeedOptions])
|
|
111
|
+
|
|
112
|
+
const initialValues = React.useMemo<WarehouseFormValues>(() => {
|
|
113
|
+
if (mode === 'edit' && row) {
|
|
114
|
+
return {
|
|
115
|
+
name: row.name || '',
|
|
116
|
+
code: row.code || '',
|
|
117
|
+
city: row.city || '',
|
|
118
|
+
country: row.country || '',
|
|
119
|
+
timezone: row.timezone || '',
|
|
120
|
+
isActive: row.is_active !== false,
|
|
121
|
+
isPrimary: row.is_primary === true,
|
|
122
|
+
...extractCustomFieldEntries(row as Record<string, unknown>),
|
|
123
|
+
} as WarehouseFormValues
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
name: '',
|
|
127
|
+
code: '',
|
|
128
|
+
city: '',
|
|
129
|
+
country: '',
|
|
130
|
+
timezone: '',
|
|
131
|
+
isActive: true,
|
|
132
|
+
isPrimary: false,
|
|
133
|
+
}
|
|
134
|
+
}, [mode, row])
|
|
135
|
+
|
|
136
|
+
const handleSubmit = React.useCallback(async (values: WarehouseFormValues) => {
|
|
137
|
+
setSubmitting(true)
|
|
138
|
+
try {
|
|
139
|
+
const payload = mode === 'edit' && row ? { id: row.id, ...values } : values
|
|
140
|
+
const call = await runMutation({
|
|
141
|
+
operation: async () => {
|
|
142
|
+
const result = await apiCall<{ id?: string | null; updatedAt?: string | null; updated_at?: string | null }>(
|
|
143
|
+
'/api/wms/warehouses',
|
|
144
|
+
{
|
|
145
|
+
method: mode === 'edit' ? 'PUT' : 'POST',
|
|
146
|
+
headers: { 'x-om-forbidden-redirect': '0' },
|
|
147
|
+
body: JSON.stringify(payload),
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
if (!result.ok) {
|
|
151
|
+
await raiseCrudError(result.response, t('wms.backend.config.warehouses.errors.save', 'Failed to save warehouse.'))
|
|
152
|
+
}
|
|
153
|
+
return result
|
|
154
|
+
},
|
|
155
|
+
context: {},
|
|
156
|
+
mutationPayload: payload,
|
|
157
|
+
})
|
|
158
|
+
flash(
|
|
159
|
+
mode === 'edit'
|
|
160
|
+
? t('wms.backend.config.warehouses.flash.updated', 'Warehouse updated')
|
|
161
|
+
: t('wms.backend.config.warehouses.flash.created', 'Warehouse created'),
|
|
162
|
+
'success',
|
|
163
|
+
)
|
|
164
|
+
const resultBody =
|
|
165
|
+
call?.result && typeof call.result === 'object'
|
|
166
|
+
? (call.result as { id?: unknown; updatedAt?: unknown; updated_at?: unknown })
|
|
167
|
+
: null
|
|
168
|
+
const createdId =
|
|
169
|
+
mode === 'create' && typeof resultBody?.id === 'string'
|
|
170
|
+
? resultBody.id.trim()
|
|
171
|
+
: undefined
|
|
172
|
+
const updatedAt =
|
|
173
|
+
(typeof resultBody?.updatedAt === 'string' && resultBody.updatedAt.trim()) ||
|
|
174
|
+
(typeof resultBody?.updated_at === 'string' && resultBody.updated_at.trim()) ||
|
|
175
|
+
undefined
|
|
176
|
+
onOpenChange(false)
|
|
177
|
+
await onSaved?.({
|
|
178
|
+
mode,
|
|
179
|
+
id: createdId || row?.id,
|
|
180
|
+
values,
|
|
181
|
+
updatedAt: updatedAt || null,
|
|
182
|
+
})
|
|
183
|
+
} catch (error) {
|
|
184
|
+
flashMutationError(error, t('wms.backend.config.warehouses.errors.save', 'Failed to save warehouse.'), t)
|
|
185
|
+
} finally {
|
|
186
|
+
setSubmitting(false)
|
|
187
|
+
}
|
|
188
|
+
}, [mode, onOpenChange, onSaved, row, runMutation, t])
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
192
|
+
<DialogContent className="max-w-3xl">
|
|
193
|
+
<DialogHeader>
|
|
194
|
+
<DialogTitle>
|
|
195
|
+
{mode === 'edit'
|
|
196
|
+
? t('wms.backend.config.warehouses.dialog.edit', 'Edit warehouse')
|
|
197
|
+
: t('wms.backend.config.warehouses.dialog.create', 'Create warehouse')}
|
|
198
|
+
</DialogTitle>
|
|
199
|
+
</DialogHeader>
|
|
200
|
+
<CrudForm<WarehouseFormValues>
|
|
201
|
+
schema={warehouseFormSchema}
|
|
202
|
+
fields={fields}
|
|
203
|
+
entityId={E.wms.warehouse}
|
|
204
|
+
initialValues={initialValues}
|
|
205
|
+
submitLabel={t('common.save', 'Save')}
|
|
206
|
+
onSubmit={handleSubmit}
|
|
207
|
+
embedded
|
|
208
|
+
disableInitialFocus
|
|
209
|
+
isLoading={submitting}
|
|
210
|
+
twoColumn
|
|
211
|
+
optimisticLockUpdatedAt={mode === 'edit' ? (row?.updatedAt ?? row?.updated_at ?? null) : undefined}
|
|
212
|
+
/>
|
|
213
|
+
</DialogContent>
|
|
214
|
+
</Dialog>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
inventoryRotationStrategyLabel,
|
|
39
39
|
} from '../../lib/inventoryDisplayUi'
|
|
40
40
|
import { LocationEditDialog } from './LocationEditDialog'
|
|
41
|
+
import { WarehouseEditDialog, type WarehouseFormValues } from './WarehouseEditDialog'
|
|
42
|
+
import { formatWarehouseCountryLabel } from './warehouseFormOptions'
|
|
41
43
|
import { useWmsInventoryMutationAccess } from './useWmsInventoryMutationAccess'
|
|
42
44
|
import { flashMutationError } from '../../lib/flashMutationError'
|
|
43
45
|
|
|
@@ -59,6 +61,9 @@ type WarehouseRow = {
|
|
|
59
61
|
timezone?: string | null
|
|
60
62
|
is_active?: boolean | null
|
|
61
63
|
is_primary?: boolean | null
|
|
64
|
+
updated_at?: string | null
|
|
65
|
+
updatedAt?: string | null
|
|
66
|
+
customValues?: Record<string, unknown> | null
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
type LocationRow = {
|
|
@@ -106,19 +111,6 @@ type InventoryProfileRow = {
|
|
|
106
111
|
safety_stock?: string | number | null
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
type WarehouseFormValues = {
|
|
110
|
-
name: string
|
|
111
|
-
code: string
|
|
112
|
-
city?: string
|
|
113
|
-
country?: string
|
|
114
|
-
timezone?: string
|
|
115
|
-
isActive: boolean
|
|
116
|
-
isPrimary: boolean
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// `id` / `updatedAt` are carried in edit mode so CrudForm recognises the form as an edit
|
|
120
|
-
// and derives the optimistic-lock header; the `cf_` index signature holds the values
|
|
121
|
-
// collected from the injected custom-field inputs.
|
|
122
114
|
type ZoneFormValues = {
|
|
123
115
|
warehouseId: string
|
|
124
116
|
code: string
|
|
@@ -144,19 +136,6 @@ type DialogMode<T> =
|
|
|
144
136
|
| { mode: 'create' }
|
|
145
137
|
| { mode: 'edit'; row: T }
|
|
146
138
|
|
|
147
|
-
const warehouseFormSchema = z.object({
|
|
148
|
-
name: z.string().trim().min(1),
|
|
149
|
-
code: z.string().trim().min(1),
|
|
150
|
-
city: z.string().trim().optional(),
|
|
151
|
-
country: z.string().trim().optional(),
|
|
152
|
-
timezone: z.string().trim().optional(),
|
|
153
|
-
isActive: z.boolean().default(true),
|
|
154
|
-
isPrimary: z.boolean().default(false),
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
// passthrough so CrudForm's schema.safeParse keeps the `cf_*` custom field values
|
|
158
|
-
// it collected from the injected custom-field inputs — a plain z.object strips them
|
|
159
|
-
// before onSubmit ever sees them.
|
|
160
139
|
const zoneFormSchema = z.object({
|
|
161
140
|
warehouseId: z.string().uuid(),
|
|
162
141
|
code: z.string().trim().min(1).max(80),
|
|
@@ -197,6 +176,7 @@ function mergeCreatedWarehouseIntoWarehousesCaches(
|
|
|
197
176
|
queryClient: QueryClient,
|
|
198
177
|
newId: string,
|
|
199
178
|
values: WarehouseFormValues,
|
|
179
|
+
updatedAt?: string | null,
|
|
200
180
|
) {
|
|
201
181
|
const row: WarehouseRow = {
|
|
202
182
|
id: newId,
|
|
@@ -207,6 +187,8 @@ function mergeCreatedWarehouseIntoWarehousesCaches(
|
|
|
207
187
|
timezone: values.timezone || null,
|
|
208
188
|
is_active: values.isActive,
|
|
209
189
|
is_primary: values.isPrimary,
|
|
190
|
+
updated_at: updatedAt ?? null,
|
|
191
|
+
updatedAt: updatedAt ?? null,
|
|
210
192
|
}
|
|
211
193
|
const haystack = `${values.name}\n${values.code}`.toLowerCase()
|
|
212
194
|
|
|
@@ -291,6 +273,7 @@ type ConfigSectionOptions = {
|
|
|
291
273
|
|
|
292
274
|
export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
293
275
|
const t = useT()
|
|
276
|
+
const locale = useLocale()
|
|
294
277
|
const queryClient = useQueryClient()
|
|
295
278
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
296
279
|
const { runMutation } = useGuardedMutation<Record<string, unknown>>({ contextId: 'wms-config-warehouses' })
|
|
@@ -298,7 +281,6 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
298
281
|
const [page, setPage] = React.useState(1)
|
|
299
282
|
const [search, setSearch] = React.useState('')
|
|
300
283
|
const [sorting, setSorting] = React.useState<SortingState>([{ id: 'updatedAt', desc: true }])
|
|
301
|
-
const [submitting, setSubmitting] = React.useState(false)
|
|
302
284
|
const [dialog, setDialog] = React.useState<DialogMode<WarehouseRow> | null>(null)
|
|
303
285
|
|
|
304
286
|
const handleSortingChange = React.useCallback((nextSorting: SortingState) => {
|
|
@@ -330,16 +312,6 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
330
312
|
},
|
|
331
313
|
})
|
|
332
314
|
|
|
333
|
-
const fields = React.useMemo<CrudField[]>(() => [
|
|
334
|
-
{ id: 'name', type: 'text', label: t('wms.backend.config.warehouses.form.name', 'Name'), required: true },
|
|
335
|
-
{ id: 'code', type: 'text', label: t('wms.backend.config.warehouses.form.code', 'Code'), required: true },
|
|
336
|
-
{ id: 'city', type: 'text', label: t('wms.backend.config.warehouses.form.city', 'City') },
|
|
337
|
-
{ id: 'country', type: 'text', label: t('wms.backend.config.warehouses.form.country', 'Country') },
|
|
338
|
-
{ id: 'timezone', type: 'text', label: t('wms.backend.config.warehouses.form.timezone', 'Timezone') },
|
|
339
|
-
{ id: 'isPrimary', type: 'checkbox', label: t('wms.backend.config.warehouses.form.primary', 'Primary warehouse') },
|
|
340
|
-
{ id: 'isActive', type: 'checkbox', label: t('wms.backend.config.warehouses.form.active', 'Active') },
|
|
341
|
-
], [t])
|
|
342
|
-
|
|
343
315
|
const columns = React.useMemo<ColumnDef<WarehouseRow>[]>(() => [
|
|
344
316
|
{
|
|
345
317
|
accessorKey: 'name',
|
|
@@ -349,7 +321,7 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
349
321
|
},
|
|
350
322
|
{ accessorKey: 'code', header: t('wms.backend.config.warehouses.columns.code', 'Code'), enableSorting: true },
|
|
351
323
|
{ accessorKey: 'city', header: t('wms.backend.config.warehouses.columns.city', 'City'), cell: ({ row }) => row.original.city || '—' },
|
|
352
|
-
{ accessorKey: 'country', header: t('wms.backend.config.warehouses.columns.country', 'Country'), cell: ({ row }) => row.original.country
|
|
324
|
+
{ accessorKey: 'country', header: t('wms.backend.config.warehouses.columns.country', 'Country'), cell: ({ row }) => formatWarehouseCountryLabel(row.original.country, locale) },
|
|
353
325
|
{
|
|
354
326
|
accessorKey: 'is_primary',
|
|
355
327
|
header: t('wms.backend.config.warehouses.columns.primary', 'Primary'),
|
|
@@ -366,34 +338,10 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
366
338
|
? t('wms.common.inactive', 'Inactive')
|
|
367
339
|
: t('wms.common.active', 'Active'),
|
|
368
340
|
},
|
|
369
|
-
], [t])
|
|
370
|
-
|
|
371
|
-
const initialValues = React.useMemo<WarehouseFormValues>(() => {
|
|
372
|
-
if (dialog?.mode === 'edit') {
|
|
373
|
-
return {
|
|
374
|
-
name: dialog.row.name || '',
|
|
375
|
-
code: dialog.row.code || '',
|
|
376
|
-
city: dialog.row.city || '',
|
|
377
|
-
country: dialog.row.country || '',
|
|
378
|
-
timezone: dialog.row.timezone || '',
|
|
379
|
-
isActive: dialog.row.is_active !== false,
|
|
380
|
-
isPrimary: dialog.row.is_primary === true,
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return {
|
|
384
|
-
name: '',
|
|
385
|
-
code: '',
|
|
386
|
-
city: '',
|
|
387
|
-
country: '',
|
|
388
|
-
timezone: '',
|
|
389
|
-
isActive: true,
|
|
390
|
-
isPrimary: false,
|
|
391
|
-
}
|
|
392
|
-
}, [dialog])
|
|
341
|
+
], [locale, t])
|
|
393
342
|
|
|
394
343
|
const closeDialog = React.useCallback(() => {
|
|
395
344
|
setDialog(null)
|
|
396
|
-
setSubmitting(false)
|
|
397
345
|
}, [])
|
|
398
346
|
|
|
399
347
|
const refresh = React.useCallback(async () => {
|
|
@@ -402,61 +350,20 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
402
350
|
await queryClient.refetchQueries({ queryKey: ['wms-config', 'warehouses'], type: 'all' })
|
|
403
351
|
}, [queryClient])
|
|
404
352
|
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
body: JSON.stringify(
|
|
417
|
-
submitMode === 'edit'
|
|
418
|
-
? { id: dialog.row.id, ...values }
|
|
419
|
-
: values,
|
|
420
|
-
),
|
|
421
|
-
},
|
|
422
|
-
)
|
|
423
|
-
if (!result.ok) {
|
|
424
|
-
await raiseCrudError(result.response, t('wms.backend.config.warehouses.errors.save', 'Failed to save warehouse.'))
|
|
425
|
-
}
|
|
426
|
-
return result
|
|
427
|
-
},
|
|
428
|
-
context: {},
|
|
429
|
-
mutationPayload: submitMode === 'edit' ? { id: dialog.row.id, ...values } : values,
|
|
430
|
-
})
|
|
431
|
-
flash(
|
|
432
|
-
submitMode === 'edit'
|
|
433
|
-
? t('wms.backend.config.warehouses.flash.updated', 'Warehouse updated')
|
|
434
|
-
: t('wms.backend.config.warehouses.flash.created', 'Warehouse created'),
|
|
435
|
-
'success',
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
const createdId =
|
|
439
|
-
submitMode === 'create' &&
|
|
440
|
-
call?.result &&
|
|
441
|
-
typeof call.result === 'object' &&
|
|
442
|
-
typeof (call.result as { id?: unknown }).id === 'string'
|
|
443
|
-
? (call.result as { id: string }).id.trim()
|
|
444
|
-
: null
|
|
445
|
-
|
|
446
|
-
closeDialog()
|
|
447
|
-
|
|
448
|
-
if (submitMode === 'create' && createdId) {
|
|
449
|
-
mergeCreatedWarehouseIntoWarehousesCaches(queryClient, createdId, values)
|
|
450
|
-
setSearch('')
|
|
451
|
-
setPage(1)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
await refresh()
|
|
455
|
-
} catch (error) {
|
|
456
|
-
flashMutationError(error, t('wms.backend.config.warehouses.errors.save', 'Failed to save warehouse.'), t)
|
|
457
|
-
setSubmitting(false)
|
|
353
|
+
const handleWarehouseSaved = React.useCallback(async (info: {
|
|
354
|
+
mode: 'create' | 'edit'
|
|
355
|
+
id?: string
|
|
356
|
+
values: WarehouseFormValues
|
|
357
|
+
updatedAt?: string | null
|
|
358
|
+
}) => {
|
|
359
|
+
closeDialog()
|
|
360
|
+
if (info.mode === 'create' && info.id) {
|
|
361
|
+
mergeCreatedWarehouseIntoWarehousesCaches(queryClient, info.id, info.values, info.updatedAt)
|
|
362
|
+
setSearch('')
|
|
363
|
+
setPage(1)
|
|
458
364
|
}
|
|
459
|
-
|
|
365
|
+
await refresh()
|
|
366
|
+
}, [closeDialog, queryClient, refresh])
|
|
460
367
|
|
|
461
368
|
const handleDelete = React.useCallback(async (row: WarehouseRow) => {
|
|
462
369
|
const confirmed = await confirm({
|
|
@@ -545,28 +452,13 @@ export function WarehouseSection({ viewAllHref }: ConfigSectionOptions = {}) {
|
|
|
545
452
|
/>
|
|
546
453
|
</SectionCard>
|
|
547
454
|
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
</DialogTitle>
|
|
556
|
-
</DialogHeader>
|
|
557
|
-
<CrudForm<WarehouseFormValues>
|
|
558
|
-
schema={warehouseFormSchema}
|
|
559
|
-
fields={fields}
|
|
560
|
-
entityId={E.wms.warehouse}
|
|
561
|
-
initialValues={initialValues}
|
|
562
|
-
submitLabel={t('common.save', 'Save')}
|
|
563
|
-
onSubmit={handleSubmit}
|
|
564
|
-
embedded
|
|
565
|
-
isLoading={submitting}
|
|
566
|
-
twoColumn
|
|
567
|
-
/>
|
|
568
|
-
</DialogContent>
|
|
569
|
-
</Dialog>
|
|
455
|
+
<WarehouseEditDialog
|
|
456
|
+
open={dialog !== null}
|
|
457
|
+
onOpenChange={(next) => { if (!next) closeDialog() }}
|
|
458
|
+
mode={dialog?.mode === 'edit' ? 'edit' : 'create'}
|
|
459
|
+
row={dialog?.mode === 'edit' ? dialog.row : null}
|
|
460
|
+
onSaved={handleWarehouseSaved}
|
|
461
|
+
/>
|
|
570
462
|
{ConfirmDialogElement}
|
|
571
463
|
</>
|
|
572
464
|
)
|