@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.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/users/route.js +13 -5
  3. package/dist/modules/auth/api/users/route.js.map +2 -2
  4. package/dist/modules/auth/lib/userIdFilter.js +16 -0
  5. package/dist/modules/auth/lib/userIdFilter.js.map +7 -0
  6. package/dist/modules/devices/acl.js +9 -1
  7. package/dist/modules/devices/acl.js.map +2 -2
  8. package/dist/modules/devices/backend/devices/[id]/page.js +4 -20
  9. package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
  10. package/dist/modules/devices/backend/devices/create/page.js +17 -1
  11. package/dist/modules/devices/backend/devices/create/page.js.map +2 -2
  12. package/dist/modules/devices/backend/devices/page.js +13 -21
  13. package/dist/modules/devices/backend/devices/page.js.map +2 -2
  14. package/dist/modules/devices/backend/devices/useDeviceUserLabels.js +34 -0
  15. package/dist/modules/devices/backend/devices/useDeviceUserLabels.js.map +7 -0
  16. package/dist/modules/devices/backend/devices/userOptions.js +52 -0
  17. package/dist/modules/devices/backend/devices/userOptions.js.map +7 -0
  18. package/dist/modules/devices/setup.js +6 -2
  19. package/dist/modules/devices/setup.js.map +2 -2
  20. package/dist/modules/wms/api/warehouseSearch.js +21 -0
  21. package/dist/modules/wms/api/warehouseSearch.js.map +7 -0
  22. package/dist/modules/wms/api/warehouses/route.js +13 -9
  23. package/dist/modules/wms/api/warehouses/route.js.map +2 -2
  24. package/dist/modules/wms/commands/configuration.js +59 -5
  25. package/dist/modules/wms/commands/configuration.js.map +2 -2
  26. package/dist/modules/wms/commands/shared.js +4 -0
  27. package/dist/modules/wms/commands/shared.js.map +2 -2
  28. package/dist/modules/wms/components/backend/WarehouseEditDialog.js +158 -0
  29. package/dist/modules/wms/components/backend/WarehouseEditDialog.js.map +7 -0
  30. package/dist/modules/wms/components/backend/WmsConfigurationPage.js +29 -103
  31. package/dist/modules/wms/components/backend/WmsConfigurationPage.js.map +2 -2
  32. package/dist/modules/wms/components/backend/warehouseFormOptions.js +66 -0
  33. package/dist/modules/wms/components/backend/warehouseFormOptions.js.map +7 -0
  34. package/package.json +7 -7
  35. package/src/modules/auth/api/users/route.ts +18 -5
  36. package/src/modules/auth/lib/userIdFilter.ts +31 -0
  37. package/src/modules/devices/AGENTS.md +12 -1
  38. package/src/modules/devices/acl.ts +9 -1
  39. package/src/modules/devices/backend/devices/[id]/page.tsx +5 -19
  40. package/src/modules/devices/backend/devices/create/page.tsx +17 -1
  41. package/src/modules/devices/backend/devices/page.tsx +20 -24
  42. package/src/modules/devices/backend/devices/useDeviceUserLabels.ts +45 -0
  43. package/src/modules/devices/backend/devices/userOptions.ts +99 -0
  44. package/src/modules/devices/i18n/de.json +3 -2
  45. package/src/modules/devices/i18n/en.json +3 -2
  46. package/src/modules/devices/i18n/es.json +3 -2
  47. package/src/modules/devices/i18n/ko.json +3 -2
  48. package/src/modules/devices/i18n/pl.json +3 -2
  49. package/src/modules/devices/setup.ts +6 -2
  50. package/src/modules/wms/api/warehouseSearch.ts +23 -0
  51. package/src/modules/wms/api/warehouses/route.ts +13 -9
  52. package/src/modules/wms/commands/configuration.ts +73 -7
  53. package/src/modules/wms/commands/shared.ts +9 -0
  54. package/src/modules/wms/components/backend/WarehouseEditDialog.tsx +216 -0
  55. package/src/modules/wms/components/backend/WmsConfigurationPage.tsx +31 -139
  56. package/src/modules/wms/components/backend/warehouseFormOptions.ts +72 -0
  57. package/src/modules/wms/i18n/de.json +2 -0
  58. package/src/modules/wms/i18n/en.json +2 -0
  59. package/src/modules/wms/i18n/es.json +2 -0
  60. package/src/modules/wms/i18n/ko.json +2 -0
  61. 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
- 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
+ }
@@ -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 handleSubmit = React.useCallback(async (values: WarehouseFormValues) => {
406
- if (!dialog) return
407
- const submitMode = dialog.mode
408
- setSubmitting(true)
409
- try {
410
- const call = await runMutation({
411
- operation: async () => {
412
- const result = await apiCall<{ id?: string | null }>(
413
- '/api/wms/warehouses',
414
- {
415
- method: submitMode === 'edit' ? 'PUT' : 'POST',
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
- }, [closeDialog, dialog, queryClient, refresh, runMutation, t])
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
- <Dialog open={dialog !== null} onOpenChange={(next) => !next && closeDialog()}>
549
- <DialogContent className="max-w-3xl">
550
- <DialogHeader>
551
- <DialogTitle>
552
- {dialog?.mode === 'edit'
553
- ? t('wms.backend.config.warehouses.dialog.edit', 'Edit warehouse')
554
- : t('wms.backend.config.warehouses.dialog.create', 'Create warehouse')}
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
  )