@open-mercato/core 0.6.8-develop.7095.1.465fbf8f80 → 0.6.8-develop.7099.1.7ba2771d06
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/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/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
|
@@ -2,9 +2,9 @@ import { z } from 'zod'
|
|
|
2
2
|
import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
|
|
3
3
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
4
|
import { parseScopedCommandInput, resolveCrudRecordId } from '@open-mercato/shared/lib/api/scoped'
|
|
5
|
-
import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
|
|
6
5
|
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
7
6
|
import { E } from '#generated/entities.ids.generated'
|
|
7
|
+
import { buildWarehouseListSearchOr } from '../warehouseSearch'
|
|
8
8
|
import { Warehouse } from '../../data/entities'
|
|
9
9
|
import { warehouseCreateSchema, warehouseUpdateSchema } from '../../data/validators'
|
|
10
10
|
import { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from '../openapi'
|
|
@@ -77,6 +77,9 @@ const crud = makeCrudRoute({
|
|
|
77
77
|
F.created_at,
|
|
78
78
|
F.updated_at,
|
|
79
79
|
],
|
|
80
|
+
decorateCustomFields: {
|
|
81
|
+
entityIds: E.wms.warehouse,
|
|
82
|
+
},
|
|
80
83
|
sortFieldMap: {
|
|
81
84
|
name: F.name,
|
|
82
85
|
code: F.code,
|
|
@@ -94,13 +97,7 @@ const crud = makeCrudRoute({
|
|
|
94
97
|
if (isActive !== null) filters[F.is_active] = { $eq: isActive }
|
|
95
98
|
const term = query.search?.trim()
|
|
96
99
|
if (term) {
|
|
97
|
-
|
|
98
|
-
filters.$or = [
|
|
99
|
-
{ [F.name]: { $ilike: like } },
|
|
100
|
-
{ [F.code]: { $ilike: like } },
|
|
101
|
-
{ [F.city]: { $ilike: like } },
|
|
102
|
-
{ [F.country]: { $ilike: like } },
|
|
103
|
-
]
|
|
100
|
+
filters.$or = buildWarehouseListSearchOr(term)
|
|
104
101
|
}
|
|
105
102
|
return filters
|
|
106
103
|
},
|
|
@@ -113,7 +110,10 @@ const crud = makeCrudRoute({
|
|
|
113
110
|
const { translate } = await resolveTranslations()
|
|
114
111
|
return parseScopedCommandInput(warehouseCreateSchema, raw ?? {}, ctx, translate)
|
|
115
112
|
},
|
|
116
|
-
response: ({ result }) => ({
|
|
113
|
+
response: ({ result }) => ({
|
|
114
|
+
id: result?.warehouseId ?? null,
|
|
115
|
+
updatedAt: result?.updatedAt ?? null,
|
|
116
|
+
}),
|
|
117
117
|
status: 201,
|
|
118
118
|
},
|
|
119
119
|
update: {
|
|
@@ -166,6 +166,10 @@ export const openApi = createWmsCrudOpenApi({
|
|
|
166
166
|
listResponseSchema: createPagedListResponseSchema(warehouseListItemSchema),
|
|
167
167
|
create: {
|
|
168
168
|
schema: warehouseCreateSchema,
|
|
169
|
+
responseSchema: z.object({
|
|
170
|
+
id: z.string().uuid().nullable(),
|
|
171
|
+
updatedAt: z.string().nullable(),
|
|
172
|
+
}),
|
|
169
173
|
description: 'Creates a warehouse for inventory operations.',
|
|
170
174
|
},
|
|
171
175
|
update: {
|
|
@@ -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
|
+
}
|