@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.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/wms/api/warehouseSearch.js +21 -0
  3. package/dist/modules/wms/api/warehouseSearch.js.map +7 -0
  4. package/dist/modules/wms/api/warehouses/route.js +13 -9
  5. package/dist/modules/wms/api/warehouses/route.js.map +2 -2
  6. package/dist/modules/wms/commands/configuration.js +59 -5
  7. package/dist/modules/wms/commands/configuration.js.map +2 -2
  8. package/dist/modules/wms/commands/shared.js +4 -0
  9. package/dist/modules/wms/commands/shared.js.map +2 -2
  10. package/dist/modules/wms/components/backend/WarehouseEditDialog.js +158 -0
  11. package/dist/modules/wms/components/backend/WarehouseEditDialog.js.map +7 -0
  12. package/dist/modules/wms/components/backend/WmsConfigurationPage.js +29 -103
  13. package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
  14. package/dist/modules/wms/components/backend/warehouseFormOptions.js +66 -0
  15. package/dist/modules/wms/components/backend/warehouseFormOptions.js.map +7 -0
  16. package/package.json +7 -7
  17. package/src/modules/wms/api/warehouseSearch.ts +23 -0
  18. package/src/modules/wms/api/warehouses/route.ts +13 -9
  19. package/src/modules/wms/commands/configuration.ts +73 -7
  20. package/src/modules/wms/commands/shared.ts +9 -0
  21. package/src/modules/wms/components/backend/WarehouseEditDialog.tsx +216 -0
  22. package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +31 -139
  23. package/src/modules/wms/components/backend/warehouseFormOptions.ts +72 -0
  24. package/src/modules/wms/i18n/de.json +2 -0
  25. package/src/modules/wms/i18n/en.json +2 -0
  26. package/src/modules/wms/i18n/es.json +2 -0
  27. package/src/modules/wms/i18n/ko.json +2 -0
  28. 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
- const like = `%${escapeLikePattern(term)}%`
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 }) => ({ id: result?.warehouseId ?? null }),
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
- return record ? snapshotWarehouse(record) : null
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.parse(rawInput ?? {})
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.parse(rawInput ?? {})
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
+ }