@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
@@ -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
  )
@@ -0,0 +1,72 @@
1
+ import type { CrudFieldOption } from '@open-mercato/ui/backend/CrudForm'
2
+ import { buildCountryOptions, resolveCountryName } from '@open-mercato/shared/lib/location/countries'
3
+
4
+ const OPTION_LIMIT = 100
5
+
6
+ function matchesQuery(haystack: string, query: string): boolean {
7
+ return haystack.toLowerCase().includes(query.toLowerCase())
8
+ }
9
+
10
+ export function countryOptionFromStored(value: string, locale?: string): CrudFieldOption {
11
+ const trimmed = value.trim()
12
+ const code = trimmed.toUpperCase()
13
+ const label = resolveCountryName(code, { locale })
14
+ if (label && label !== code) {
15
+ return { value: code, label }
16
+ }
17
+ return { value: trimmed, label: trimmed }
18
+ }
19
+
20
+ export function formatWarehouseCountryLabel(value: string | null | undefined, locale?: string): string {
21
+ const trimmed = value?.trim()
22
+ if (!trimmed) return '—'
23
+ return countryOptionFromStored(trimmed, locale).label
24
+ }
25
+
26
+ export function timezoneOptionFromStored(value: string): CrudFieldOption {
27
+ const trimmed = value.trim()
28
+ return { value: trimmed, label: trimmed }
29
+ }
30
+
31
+ export async function loadCountryOptions(query?: string, locale?: string): Promise<CrudFieldOption[]> {
32
+ const options = buildCountryOptions({ locale }).map((entry) => ({
33
+ value: entry.code,
34
+ label: entry.label,
35
+ }))
36
+ const term = query?.trim()
37
+ const filtered = term
38
+ ? options.filter((option) => matchesQuery(option.label, term) || matchesQuery(option.value, term))
39
+ : options
40
+ return filtered.slice(0, OPTION_LIMIT)
41
+ }
42
+
43
+ function resolveLocalTimeZone(): string | undefined {
44
+ try {
45
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || undefined
46
+ } catch {
47
+ return undefined
48
+ }
49
+ }
50
+
51
+ function listSupportedTimeZones(): string[] {
52
+ try {
53
+ const intlWithSupportedValues = Intl as typeof Intl & { supportedValuesOf?: (input: 'timeZone') => string[] }
54
+ if (typeof intlWithSupportedValues.supportedValuesOf !== 'function') return []
55
+ return intlWithSupportedValues.supportedValuesOf('timeZone').filter(Boolean)
56
+ } catch {
57
+ return []
58
+ }
59
+ }
60
+
61
+ export async function loadTimezoneOptions(query?: string): Promise<CrudFieldOption[]> {
62
+ const localZone = resolveLocalTimeZone()
63
+ const preferred = localZone && localZone !== 'UTC' ? ['UTC', localZone] : ['UTC']
64
+ const zones = new Set<string>([...preferred, ...listSupportedTimeZones()])
65
+ const rest = Array.from(zones).filter((tz) => !preferred.includes(tz))
66
+ const allTz = [...preferred, ...rest]
67
+ const term = query?.trim()
68
+ const filtered = term
69
+ ? allTz.filter((tz) => matchesQuery(tz, term))
70
+ : allTz
71
+ return filtered.slice(0, OPTION_LIMIT).map((tz) => ({ value: tz, label: tz }))
72
+ }
@@ -95,9 +95,11 @@
95
95
  "wms.backend.config.warehouses.form.city": "Stadt",
96
96
  "wms.backend.config.warehouses.form.code": "Code",
97
97
  "wms.backend.config.warehouses.form.country": "Land",
98
+ "wms.backend.config.warehouses.form.countryPlaceholder": "Land suchen",
98
99
  "wms.backend.config.warehouses.form.name": "Name",
99
100
  "wms.backend.config.warehouses.form.primary": "Hauptlager",
100
101
  "wms.backend.config.warehouses.form.timezone": "Zeitzone",
102
+ "wms.backend.config.warehouses.form.timezonePlaceholder": "Zeitzone suchen",
101
103
  "wms.backend.config.warehouses.primary.yes": "Primär",
102
104
  "wms.backend.config.warehouses.search": "Lager suchen",
103
105
  "wms.backend.config.warehouses.title": "Lager",
@@ -95,9 +95,11 @@
95
95
  "wms.backend.config.warehouses.form.city": "City",
96
96
  "wms.backend.config.warehouses.form.code": "Code",
97
97
  "wms.backend.config.warehouses.form.country": "Country",
98
+ "wms.backend.config.warehouses.form.countryPlaceholder": "Search country",
98
99
  "wms.backend.config.warehouses.form.name": "Name",
99
100
  "wms.backend.config.warehouses.form.primary": "Primary warehouse",
100
101
  "wms.backend.config.warehouses.form.timezone": "Timezone",
102
+ "wms.backend.config.warehouses.form.timezonePlaceholder": "Search timezone",
101
103
  "wms.backend.config.warehouses.primary.yes": "Primary",
102
104
  "wms.backend.config.warehouses.search": "Search warehouses",
103
105
  "wms.backend.config.warehouses.title": "Warehouses",
@@ -95,9 +95,11 @@
95
95
  "wms.backend.config.warehouses.form.city": "Ciudad",
96
96
  "wms.backend.config.warehouses.form.code": "Código",
97
97
  "wms.backend.config.warehouses.form.country": "País",
98
+ "wms.backend.config.warehouses.form.countryPlaceholder": "Buscar país",
98
99
  "wms.backend.config.warehouses.form.name": "Nombre",
99
100
  "wms.backend.config.warehouses.form.primary": "Almacén principal",
100
101
  "wms.backend.config.warehouses.form.timezone": "Zona horaria",
102
+ "wms.backend.config.warehouses.form.timezonePlaceholder": "Buscar zona horaria",
101
103
  "wms.backend.config.warehouses.primary.yes": "Principal",
102
104
  "wms.backend.config.warehouses.search": "Buscar almacenes",
103
105
  "wms.backend.config.warehouses.title": "Almacenes",
@@ -95,9 +95,11 @@
95
95
  "wms.backend.config.warehouses.form.city": "도시",
96
96
  "wms.backend.config.warehouses.form.code": "암호",
97
97
  "wms.backend.config.warehouses.form.country": "국가",
98
+ "wms.backend.config.warehouses.form.countryPlaceholder": "국가 검색",
98
99
  "wms.backend.config.warehouses.form.name": "이름",
99
100
  "wms.backend.config.warehouses.form.primary": "1차 창고",
100
101
  "wms.backend.config.warehouses.form.timezone": "시간대",
102
+ "wms.backend.config.warehouses.form.timezonePlaceholder": "시간대 검색",
101
103
  "wms.backend.config.warehouses.primary.yes": "주요한",
102
104
  "wms.backend.config.warehouses.search": "창고 검색",
103
105
  "wms.backend.config.warehouses.title": "창고",
@@ -95,9 +95,11 @@
95
95
  "wms.backend.config.warehouses.form.city": "Miasto",
96
96
  "wms.backend.config.warehouses.form.code": "Kod",
97
97
  "wms.backend.config.warehouses.form.country": "Kraj",
98
+ "wms.backend.config.warehouses.form.countryPlaceholder": "Szukaj kraju",
98
99
  "wms.backend.config.warehouses.form.name": "Nazwa",
99
100
  "wms.backend.config.warehouses.form.primary": "Magazyn główny",
100
101
  "wms.backend.config.warehouses.form.timezone": "Strefa czasowa",
102
+ "wms.backend.config.warehouses.form.timezonePlaceholder": "Szukaj strefy czasowej",
101
103
  "wms.backend.config.warehouses.primary.yes": "Główny",
102
104
  "wms.backend.config.warehouses.search": "Szukaj magazynów",
103
105
  "wms.backend.config.warehouses.title": "Magazyny",