@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
@@ -11,6 +11,8 @@ import { flash } from "@open-mercato/ui/backend/FlashMessages";
11
11
  import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
12
12
  import { useT } from "@open-mercato/shared/lib/i18n/context";
13
13
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
14
+ import { loadDeviceUserOptions } from "./userOptions.js";
15
+ import { useDeviceUserLabels } from "./useDeviceUserLabels.js";
14
16
  function formatDate(value, t) {
15
17
  if (!value) return t("devices.list.noValue");
16
18
  try {
@@ -34,31 +36,19 @@ function DevicesAdminListPage() {
34
36
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
35
37
  const [filterValues, setFilterValues] = React.useState({});
36
38
  const [userOptions, setUserOptions] = React.useState([]);
37
- const loadUserOptions = React.useCallback(async (query) => {
38
- const params = new URLSearchParams();
39
- params.set("page", "1");
40
- params.set("pageSize", "20");
41
- if (query && query.trim().length > 0) params.set("search", query.trim());
42
- const call = await apiCall(
43
- `/api/auth/users?${params.toString()}`,
44
- { headers: { "x-om-forbidden-redirect": "0" } },
45
- { fallback: null }
46
- ).catch(() => null);
47
- if (!call || !call.ok) return [];
48
- const next = (call.result?.items ?? []).flatMap((item) => {
49
- if (!item || typeof item.id !== "string" || !item.id.trim()) return [];
50
- const name = typeof item.name === "string" && item.name.trim() ? item.name.trim() : null;
51
- const email = typeof item.email === "string" && item.email.trim() ? item.email.trim() : null;
52
- const label = name ?? email ?? item.id;
53
- return [{ value: item.id, label, description: email && email !== label ? email : null }];
54
- });
39
+ const mergeUserOptions = React.useCallback((next) => {
40
+ if (next.length === 0) return;
55
41
  setUserOptions((prev) => {
56
42
  const map = new Map(prev.map((opt) => [opt.value, opt]));
57
43
  for (const opt of next) map.set(opt.value, opt);
58
44
  return Array.from(map.values());
59
45
  });
60
- return next;
61
46
  }, []);
47
+ const loadUserOptions = React.useCallback(async (query) => {
48
+ const next = await loadDeviceUserOptions(query);
49
+ mergeUserOptions(next);
50
+ return next;
51
+ }, [mergeUserOptions]);
62
52
  React.useEffect(() => {
63
53
  void loadUserOptions();
64
54
  }, [loadUserOptions, scopeVersion]);
@@ -66,6 +56,8 @@ function DevicesAdminListPage() {
66
56
  () => new Map(userOptions.map((opt) => [opt.value, opt.label])),
67
57
  [userOptions]
68
58
  );
59
+ const rowUserIds = React.useMemo(() => rows.map((row) => row.userId), [rows]);
60
+ const resolvedUserLabels = useDeviceUserLabels(rowUserIds);
69
61
  const filters = React.useMemo(() => [
70
62
  {
71
63
  id: "platform",
@@ -163,7 +155,7 @@ function DevicesAdminListPage() {
163
155
  header: t("devices.list.columns.user"),
164
156
  cell: ({ row }) => {
165
157
  const userId = row.original.userId;
166
- const label = userLabelById.get(userId);
158
+ const label = resolvedUserLabels[userId] ?? userLabelById.get(userId);
167
159
  return (
168
160
  // Stop the click bubbling to the row, whose default action navigates to the device edit page.
169
161
  /* @__PURE__ */ jsx(
@@ -198,7 +190,7 @@ function DevicesAdminListPage() {
198
190
  header: t("devices.list.columns.lastSeen"),
199
191
  cell: ({ row }) => formatDate(row.original.lastSeenAt, t)
200
192
  }
201
- ], [t, userLabelById]);
193
+ ], [t, userLabelById, resolvedUserLabels]);
202
194
  return /* @__PURE__ */ jsxs(Page, { children: [
203
195
  /* @__PURE__ */ jsx(PageBody, { children: /* @__PURE__ */ jsx(
204
196
  DataTable,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/devices/backend/devices/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\n\ntype Row = {\n id: string\n userId: string\n deviceId: string\n platform: string\n clientAppVersion: string | null\n osVersion: string | null\n pushProvider: string | null\n pushTokenUpdatedAt: string | null\n lastSeenAt: string | null\n createdAt: string | null\n}\n\ntype ResponsePayload = {\n items: Row[]\n total: number\n page?: number\n pageSize?: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nfunction formatDate(value: string | null, t: (key: string) => string) {\n if (!value) return t('devices.list.noValue')\n try {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return t('devices.list.noValue')\n return date.toLocaleString()\n } catch {\n return t('devices.list.noValue')\n }\n}\n\nexport default function DevicesAdminListPage() {\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [userOptions, setUserOptions] = React.useState<{ value: string; label: string; description?: string | null }[]>([])\n\n // Devices admins may not hold auth.users.list; degrade gracefully (no options) instead of redirecting.\n const loadUserOptions = React.useCallback(async (query?: string) => {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', '20')\n if (query && query.trim().length > 0) params.set('search', query.trim())\n const call = await apiCall<{ items?: { id: string; name?: string | null; email?: string | null }[] }>(\n `/api/auth/users?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n ).catch(() => null)\n if (!call || !call.ok) return []\n const next = (call.result?.items ?? []).flatMap((item) => {\n if (!item || typeof item.id !== 'string' || !item.id.trim()) return []\n const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim() ? item.email.trim() : null\n const label = name ?? email ?? item.id\n return [{ value: item.id, label, description: email && email !== label ? email : null }]\n })\n setUserOptions((prev) => {\n const map = new Map(prev.map((opt) => [opt.value, opt]))\n for (const opt of next) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n return next\n }, [])\n\n React.useEffect(() => { void loadUserOptions() }, [loadUserOptions, scopeVersion])\n\n // Reuse the picker cache to label the User column; rows whose owner isn't cached still link by id.\n const userLabelById = React.useMemo(\n () => new Map(userOptions.map((opt) => [opt.value, opt.label])),\n [userOptions],\n )\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'platform',\n label: t('devices.list.columns.platform'),\n type: 'select',\n options: [\n { value: 'ios', label: 'iOS' },\n { value: 'android', label: 'Android' },\n { value: 'web', label: 'Web' },\n ],\n },\n {\n id: 'userId',\n label: t('devices.list.columns.user'),\n type: 'combobox',\n options: userOptions,\n loadOptions: loadUserOptions,\n },\n ], [t, userOptions, loadUserOptions])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '50')\n const platform = typeof filterValues.platform === 'string' ? filterValues.platform.trim() : ''\n const userId = typeof filterValues.userId === 'string' ? filterValues.userId.trim() : ''\n if (platform) params.set('platform', platform)\n if (userId) params.set('userId', userId)\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(`/api/devices/admin/devices?${params.toString()}`, undefined, { fallback })\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('devices.list.error.loadFailed')\n flash(message, 'error')\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n }\n } catch (error) {\n if (!cancelled) {\n const message = error instanceof Error ? error.message : t('devices.list.error.loadFailed')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, reloadToken, scopeVersion, filterValues, t])\n\n const handleDeactivate = React.useCallback(async (row: Row) => {\n const confirmed = await confirm({\n title: t('devices.list.confirmDeactivate'),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n // optimistic-lock-exempt: device deactivate is an idempotent soft-delete of a registry row, not a concurrent field edit\n const call = await apiCall<{ error?: string }>(\n `/api/devices/admin/devices/${encodeURIComponent(row.id)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('devices.list.error.deactivateFailed')\n flash(message, 'error')\n return\n }\n flash(t('devices.list.success.deactivated'), 'success')\n setReloadToken((token) => token + 1)\n } catch (error) {\n const message = error instanceof Error ? error.message : t('devices.list.error.deactivateFailed')\n flash(message, 'error')\n }\n }, [confirm, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => [\n {\n accessorKey: 'deviceId',\n header: t('devices.list.columns.device'),\n cell: ({ row }) => <code className=\"text-xs\">{row.original.deviceId}</code>,\n },\n { accessorKey: 'platform', header: t('devices.list.columns.platform') },\n {\n accessorKey: 'userId',\n header: t('devices.list.columns.user'),\n cell: ({ row }) => {\n const userId = row.original.userId\n const label = userLabelById.get(userId)\n return (\n // Stop the click bubbling to the row, whose default action navigates to the device edit page.\n <Link\n href={`/backend/users/${encodeURIComponent(userId)}/edit`}\n className=\"text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {label ?? <code className=\"text-xs\">{userId}</code>}\n </Link>\n )\n },\n },\n {\n accessorKey: 'clientAppVersion',\n header: t('devices.list.columns.appVersion'),\n cell: ({ row }) => row.original.clientAppVersion || t('devices.list.noValue'),\n },\n {\n accessorKey: 'osVersion',\n header: t('devices.list.columns.osVersion'),\n cell: ({ row }) => row.original.osVersion || t('devices.list.noValue'),\n },\n {\n accessorKey: 'pushProvider',\n header: t('devices.list.columns.pushProvider'),\n cell: ({ row }) => row.original.pushProvider || t('devices.list.noValue'),\n },\n {\n accessorKey: 'lastSeenAt',\n header: t('devices.list.columns.lastSeen'),\n cell: ({ row }) => formatDate(row.original.lastSeenAt, t),\n },\n ], [t, userLabelById])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('devices.list.title')}\n actions={(\n <Button asChild>\n <Link href=\"/backend/devices/create\">{t('devices.list.actions.register')}</Link>\n </Button>\n )}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n perspective={{ tableId: 'devices.list' }}\n rowActions={(row) => (\n <RowActions items={[\n { id: 'edit', label: t('devices.list.actions.edit'), href: `/backend/devices/${row.id}` },\n { id: 'deactivate', label: t('devices.list.actions.deactivate'), destructive: true, onSelect: () => { void handleDeactivate(row) } },\n ]} />\n )}\n pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\nimport type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'\nimport { loadDeviceUserOptions, type DeviceUserOption } from './userOptions'\nimport { useDeviceUserLabels } from './useDeviceUserLabels'\n\ntype Row = {\n id: string\n userId: string\n deviceId: string\n platform: string\n clientAppVersion: string | null\n osVersion: string | null\n pushProvider: string | null\n pushTokenUpdatedAt: string | null\n lastSeenAt: string | null\n createdAt: string | null\n}\n\ntype ResponsePayload = {\n items: Row[]\n total: number\n page?: number\n pageSize?: number\n totalPages: number\n totalIsCapped?: boolean\n}\n\nfunction formatDate(value: string | null, t: (key: string) => string) {\n if (!value) return t('devices.list.noValue')\n try {\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return t('devices.list.noValue')\n return date.toLocaleString()\n } catch {\n return t('devices.list.noValue')\n }\n}\n\nexport default function DevicesAdminListPage() {\n const [rows, setRows] = React.useState<Row[]>([])\n const [page, setPage] = React.useState(1)\n const [total, setTotal] = React.useState(0)\n const [totalPages, setTotalPages] = React.useState(1)\n const [totalIsCapped, setTotalIsCapped] = React.useState(false)\n const [isLoading, setIsLoading] = React.useState(true)\n const [reloadToken, setReloadToken] = React.useState(0)\n const scopeVersion = useOrganizationScopeVersion()\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const [filterValues, setFilterValues] = React.useState<FilterValues>({})\n const [userOptions, setUserOptions] = React.useState<DeviceUserOption[]>([])\n\n const mergeUserOptions = React.useCallback((next: DeviceUserOption[]) => {\n if (next.length === 0) return\n setUserOptions((prev) => {\n const map = new Map(prev.map((opt) => [opt.value, opt]))\n for (const opt of next) map.set(opt.value, opt)\n return Array.from(map.values())\n })\n }, [])\n\n // Devices admins may not hold auth.users.list; the helper degrades to no options instead of\n // redirecting the whole page to /login.\n const loadUserOptions = React.useCallback(async (query?: string) => {\n const next = await loadDeviceUserOptions(query)\n mergeUserOptions(next)\n return next\n }, [mergeUserOptions])\n\n React.useEffect(() => { void loadUserOptions() }, [loadUserOptions, scopeVersion])\n\n const userLabelById = React.useMemo(\n () => new Map(userOptions.map((opt) => [opt.value, opt.label])),\n [userOptions],\n )\n\n // The picker only ever caches the users it happened to prefetch, so resolve the owners of the rows\n // actually on this page. Without it most rows render a bare UUID.\n const rowUserIds = React.useMemo(() => rows.map((row) => row.userId), [rows])\n const resolvedUserLabels = useDeviceUserLabels(rowUserIds)\n\n const filters = React.useMemo<FilterDef[]>(() => [\n {\n id: 'platform',\n label: t('devices.list.columns.platform'),\n type: 'select',\n options: [\n { value: 'ios', label: 'iOS' },\n { value: 'android', label: 'Android' },\n { value: 'web', label: 'Web' },\n ],\n },\n {\n id: 'userId',\n label: t('devices.list.columns.user'),\n type: 'combobox',\n options: userOptions,\n loadOptions: loadUserOptions,\n },\n ], [t, userOptions, loadUserOptions])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n try {\n const params = new URLSearchParams()\n params.set('page', String(page))\n params.set('pageSize', '50')\n const platform = typeof filterValues.platform === 'string' ? filterValues.platform.trim() : ''\n const userId = typeof filterValues.userId === 'string' ? filterValues.userId.trim() : ''\n if (platform) params.set('platform', platform)\n if (userId) params.set('userId', userId)\n const fallback: ResponsePayload = { items: [], total: 0, page, totalPages: 1 }\n const call = await apiCall<ResponsePayload>(`/api/devices/admin/devices?${params.toString()}`, undefined, { fallback })\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('devices.list.error.loadFailed')\n flash(message, 'error')\n return\n }\n const payload = call.result ?? fallback\n if (!cancelled) {\n setRows(Array.isArray(payload.items) ? payload.items : [])\n setTotal(payload.total || 0)\n setTotalPages(payload.totalPages || 1)\n setTotalIsCapped(payload?.totalIsCapped === true)\n }\n } catch (error) {\n if (!cancelled) {\n const message = error instanceof Error ? error.message : t('devices.list.error.loadFailed')\n flash(message, 'error')\n }\n } finally {\n if (!cancelled) setIsLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [page, reloadToken, scopeVersion, filterValues, t])\n\n const handleDeactivate = React.useCallback(async (row: Row) => {\n const confirmed = await confirm({\n title: t('devices.list.confirmDeactivate'),\n variant: 'destructive',\n })\n if (!confirmed) return\n try {\n // optimistic-lock-exempt: device deactivate is an idempotent soft-delete of a registry row, not a concurrent field edit\n const call = await apiCall<{ error?: string }>(\n `/api/devices/admin/devices/${encodeURIComponent(row.id)}`,\n { method: 'DELETE' },\n { fallback: null },\n )\n if (!call.ok) {\n const errorPayload = call.result as { error?: string } | undefined\n const message = typeof errorPayload?.error === 'string' ? errorPayload.error : t('devices.list.error.deactivateFailed')\n flash(message, 'error')\n return\n }\n flash(t('devices.list.success.deactivated'), 'success')\n setReloadToken((token) => token + 1)\n } catch (error) {\n const message = error instanceof Error ? error.message : t('devices.list.error.deactivateFailed')\n flash(message, 'error')\n }\n }, [confirm, t])\n\n const columns = React.useMemo<ColumnDef<Row>[]>(() => [\n {\n accessorKey: 'deviceId',\n header: t('devices.list.columns.device'),\n cell: ({ row }) => <code className=\"text-xs\">{row.original.deviceId}</code>,\n },\n { accessorKey: 'platform', header: t('devices.list.columns.platform') },\n {\n accessorKey: 'userId',\n header: t('devices.list.columns.user'),\n cell: ({ row }) => {\n const userId = row.original.userId\n const label = resolvedUserLabels[userId] ?? userLabelById.get(userId)\n return (\n // Stop the click bubbling to the row, whose default action navigates to the device edit page.\n <Link\n href={`/backend/users/${encodeURIComponent(userId)}/edit`}\n className=\"text-primary hover:underline\"\n onClick={(e) => e.stopPropagation()}\n >\n {label ?? <code className=\"text-xs\">{userId}</code>}\n </Link>\n )\n },\n },\n {\n accessorKey: 'clientAppVersion',\n header: t('devices.list.columns.appVersion'),\n cell: ({ row }) => row.original.clientAppVersion || t('devices.list.noValue'),\n },\n {\n accessorKey: 'osVersion',\n header: t('devices.list.columns.osVersion'),\n cell: ({ row }) => row.original.osVersion || t('devices.list.noValue'),\n },\n {\n accessorKey: 'pushProvider',\n header: t('devices.list.columns.pushProvider'),\n cell: ({ row }) => row.original.pushProvider || t('devices.list.noValue'),\n },\n {\n accessorKey: 'lastSeenAt',\n header: t('devices.list.columns.lastSeen'),\n cell: ({ row }) => formatDate(row.original.lastSeenAt, t),\n },\n ], [t, userLabelById, resolvedUserLabels])\n\n return (\n <Page>\n <PageBody>\n <DataTable\n title={t('devices.list.title')}\n actions={(\n <Button asChild>\n <Link href=\"/backend/devices/create\">{t('devices.list.actions.register')}</Link>\n </Button>\n )}\n columns={columns}\n data={rows}\n filters={filters}\n filterValues={filterValues}\n onFiltersApply={(values) => { setFilterValues(values); setPage(1) }}\n onFiltersClear={() => { setFilterValues({}); setPage(1) }}\n perspective={{ tableId: 'devices.list' }}\n rowActions={(row) => (\n <RowActions items={[\n { id: 'edit', label: t('devices.list.actions.edit'), href: `/backend/devices/${row.id}` },\n { id: 'deactivate', label: t('devices.list.actions.deactivate'), destructive: true, onSelect: () => { void handleDeactivate(row) } },\n ]} />\n )}\n pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}\n isLoading={isLoading}\n />\n </PageBody>\n {ConfirmDialogElement}\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { resolveDeviceUserOptions } from "./userOptions.js";
4
+ function useDeviceUserLabels(userIds) {
5
+ const [labels, setLabels] = React.useState({});
6
+ const resolvedIdsRef = React.useRef(/* @__PURE__ */ new Set());
7
+ const idsKey = React.useMemo(() => {
8
+ const normalized = /* @__PURE__ */ new Set();
9
+ for (const userId of userIds) {
10
+ if (typeof userId === "string" && userId.trim()) normalized.add(userId.trim());
11
+ }
12
+ return Array.from(normalized).sort((left, right) => left < right ? -1 : left > right ? 1 : 0).join(",");
13
+ }, [userIds]);
14
+ React.useEffect(() => {
15
+ if (!idsKey) return;
16
+ const unresolved = idsKey.split(",").filter((userId) => !resolvedIdsRef.current.has(userId));
17
+ if (unresolved.length === 0) return;
18
+ const controller = new AbortController();
19
+ void resolveDeviceUserOptions(unresolved, controller.signal).then(({ options, resolvedIds }) => {
20
+ if (controller.signal.aborted) return;
21
+ for (const userId of resolvedIds) resolvedIdsRef.current.add(userId);
22
+ const next = {};
23
+ for (const option of options) next[option.value] = option.label;
24
+ if (Object.keys(next).length) setLabels((current) => ({ ...current, ...next }));
25
+ }).catch(() => {
26
+ });
27
+ return () => controller.abort();
28
+ }, [idsKey]);
29
+ return labels;
30
+ }
31
+ export {
32
+ useDeviceUserLabels
33
+ };
34
+ //# sourceMappingURL=useDeviceUserLabels.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/devices/backend/devices/useDeviceUserLabels.ts"],
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { resolveDeviceUserOptions } from './userOptions'\n\n// Resolves owner ids that are on screen into display labels. Modelled on\n// warranty_claims/backend/components/useUserDisplayNames, but every failure degrades to an empty\n// map instead of throwing: a devices admin without `auth.users.list` must still see the page.\nexport function useDeviceUserLabels(userIds: readonly (string | null | undefined)[]): Record<string, string> {\n const [labels, setLabels] = React.useState<Record<string, string>>({})\n const resolvedIdsRef = React.useRef<Set<string>>(new Set())\n\n const idsKey = React.useMemo(() => {\n const normalized = new Set<string>()\n for (const userId of userIds) {\n if (typeof userId === 'string' && userId.trim()) normalized.add(userId.trim())\n }\n return Array.from(normalized)\n .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))\n .join(',')\n }, [userIds])\n\n React.useEffect(() => {\n if (!idsKey) return\n const unresolved = idsKey.split(',').filter((userId) => !resolvedIdsRef.current.has(userId))\n if (unresolved.length === 0) return\n\n const controller = new AbortController()\n void resolveDeviceUserOptions(unresolved, controller.signal)\n .then(({ options, resolvedIds }) => {\n if (controller.signal.aborted) return\n // Only ids the server actually answered for are remembered. Marking an id whose request\n // failed would keep its row showing a bare UUID for the life of the component, even though\n // the next attempt would have worked.\n for (const userId of resolvedIds) resolvedIdsRef.current.add(userId)\n const next: Record<string, string> = {}\n for (const option of options) next[option.value] = option.label\n if (Object.keys(next).length) setLabels((current) => ({ ...current, ...next }))\n })\n .catch(() => {})\n return () => controller.abort()\n }, [idsKey])\n\n return labels\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,gCAAgC;AAKlC,SAAS,oBAAoB,SAAyE;AAC3G,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAiC,CAAC,CAAC;AACrE,QAAM,iBAAiB,MAAM,OAAoB,oBAAI,IAAI,CAAC;AAE1D,QAAM,SAAS,MAAM,QAAQ,MAAM;AACjC,UAAM,aAAa,oBAAI,IAAY;AACnC,eAAW,UAAU,SAAS;AAC5B,UAAI,OAAO,WAAW,YAAY,OAAO,KAAK,EAAG,YAAW,IAAI,OAAO,KAAK,CAAC;AAAA,IAC/E;AACA,WAAO,MAAM,KAAK,UAAU,EACzB,KAAK,CAAC,MAAM,UAAW,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE,EAChE,KAAK,GAAG;AAAA,EACb,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,UAAU,MAAM;AACpB,QAAI,CAAC,OAAQ;AACb,UAAM,aAAa,OAAO,MAAM,GAAG,EAAE,OAAO,CAAC,WAAW,CAAC,eAAe,QAAQ,IAAI,MAAM,CAAC;AAC3F,QAAI,WAAW,WAAW,EAAG;AAE7B,UAAM,aAAa,IAAI,gBAAgB;AACvC,SAAK,yBAAyB,YAAY,WAAW,MAAM,EACxD,KAAK,CAAC,EAAE,SAAS,YAAY,MAAM;AAClC,UAAI,WAAW,OAAO,QAAS;AAI/B,iBAAW,UAAU,YAAa,gBAAe,QAAQ,IAAI,MAAM;AACnE,YAAM,OAA+B,CAAC;AACtC,iBAAW,UAAU,QAAS,MAAK,OAAO,KAAK,IAAI,OAAO;AAC1D,UAAI,OAAO,KAAK,IAAI,EAAE,OAAQ,WAAU,CAAC,aAAa,EAAE,GAAG,SAAS,GAAG,KAAK,EAAE;AAAA,IAChF,CAAC,EACA,MAAM,MAAM;AAAA,IAAC,CAAC;AACjB,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,GAAG,CAAC,MAAM,CAAC;AAEX,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1,52 @@
1
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
2
+ import { MAX_USER_LOOKUP_IDS } from "@open-mercato/core/modules/auth/lib/userIdFilter";
3
+ const SEARCH_PAGE_SIZE = 20;
4
+ const MAX_IDS_PER_LOOKUP = MAX_USER_LOOKUP_IDS;
5
+ function toOption(item, style) {
6
+ if (!item || typeof item.id !== "string" || !item.id.trim()) return [];
7
+ const id = item.id.trim();
8
+ const name = typeof item.name === "string" && item.name.trim() ? item.name.trim() : null;
9
+ const email = typeof item.email === "string" && item.email.trim() ? item.email.trim() : null;
10
+ const label = style === "search" && name && email ? `${name} \u2014 ${email}` : name ?? email ?? id;
11
+ return [{ value: id, label }];
12
+ }
13
+ async function fetchUsers(params, style, signal) {
14
+ const call = await apiCall(
15
+ `/api/auth/users?${params.toString()}`,
16
+ { headers: { "x-om-forbidden-redirect": "0" }, signal },
17
+ { fallback: null }
18
+ ).catch(() => null);
19
+ if (!call || !call.ok) return null;
20
+ return (call.result?.items ?? []).flatMap((item) => toOption(item, style));
21
+ }
22
+ async function loadDeviceUserOptions(query) {
23
+ const params = new URLSearchParams();
24
+ params.set("page", "1");
25
+ params.set("pageSize", String(SEARCH_PAGE_SIZE));
26
+ const trimmed = query?.trim();
27
+ if (trimmed) params.set("search", trimmed);
28
+ return await fetchUsers(params, "search") ?? [];
29
+ }
30
+ async function resolveDeviceUserOptions(ids, signal) {
31
+ const unique = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
32
+ if (unique.length === 0) return { options: [], resolvedIds: [] };
33
+ const options = [];
34
+ const resolvedIds = [];
35
+ for (let offset = 0; offset < unique.length; offset += MAX_IDS_PER_LOOKUP) {
36
+ const batch = unique.slice(offset, offset + MAX_IDS_PER_LOOKUP);
37
+ const params = new URLSearchParams();
38
+ params.set("page", "1");
39
+ params.set("pageSize", String(batch.length));
40
+ params.set("ids", batch.join(","));
41
+ const batchOptions = await fetchUsers(params, "compact", signal);
42
+ if (batchOptions === null) continue;
43
+ options.push(...batchOptions);
44
+ resolvedIds.push(...batch);
45
+ }
46
+ return { options, resolvedIds };
47
+ }
48
+ export {
49
+ loadDeviceUserOptions,
50
+ resolveDeviceUserOptions
51
+ };
52
+ //# sourceMappingURL=userOptions.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/devices/backend/devices/userOptions.ts"],
4
+ "sourcesContent": ["import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { MAX_USER_LOOKUP_IDS } from '@open-mercato/core/modules/auth/lib/userIdFilter'\n\nexport type DeviceUserOption = {\n value: string\n label: string\n}\n\ntype AuthUserItem = {\n id?: unknown\n name?: unknown\n email?: unknown\n}\n\nconst SEARCH_PAGE_SIZE = 20\n// Imported rather than restated: `parseIdsParam` slices past the route's cap without complaining,\n// so a client batch larger than the server accepts loses the overflow ids silently.\nconst MAX_IDS_PER_LOOKUP = MAX_USER_LOOKUP_IDS\n\n/**\n * Two label styles on purpose. A picker suggestion has to disambiguate two people with the same\n * display name, and neither `CrudForm`'s combobox nor `FilterBar` renders anything but the label \u2014\n * so the email has to live in the label there. A resolved column label is read next to a device,\n * where the email is noise.\n */\ntype LabelStyle = 'search' | 'compact'\n\nfunction toOption(item: AuthUserItem | null | undefined, style: LabelStyle): DeviceUserOption[] {\n if (!item || typeof item.id !== 'string' || !item.id.trim()) return []\n const id = item.id.trim()\n const name = typeof item.name === 'string' && item.name.trim() ? item.name.trim() : null\n const email = typeof item.email === 'string' && item.email.trim() ? item.email.trim() : null\n const label = style === 'search' && name && email ? `${name} \u2014 ${email}` : name ?? email ?? id\n return [{ value: id, label }]\n}\n\n// `devices.admin` declares `dependsOn: ['auth.users.list']`, but dependsOn only diagnoses \u2014 a\n// hand-built role can still reach these screens without it until the ACL editor or\n// `sync-role-acls` fixes it. `x-om-forbidden-redirect: 0` keeps that 403 from bouncing the whole\n// page to /login. `null` means the call itself failed, which callers must not\n// confuse with a successful call that matched nobody \u2014 a caller caching \"already resolved\" would\n// otherwise remember a transient network error forever.\nasync function fetchUsers(\n params: URLSearchParams,\n style: LabelStyle,\n signal?: AbortSignal,\n): Promise<DeviceUserOption[] | null> {\n const call = await apiCall<{ items?: AuthUserItem[] }>(\n `/api/auth/users?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' }, signal },\n { fallback: null },\n ).catch(() => null)\n if (!call || !call.ok) return null\n return (call.result?.items ?? []).flatMap((item) => toOption(item, style))\n}\n\nexport async function loadDeviceUserOptions(query?: string): Promise<DeviceUserOption[]> {\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', String(SEARCH_PAGE_SIZE))\n const trimmed = query?.trim()\n if (trimmed) params.set('search', trimmed)\n // A picker has nothing to cache, so a failed lookup is just an empty suggestion list.\n return (await fetchUsers(params, 'search')) ?? []\n}\n\n/**\n * The outcome of a batch lookup. `resolvedIds` lists the ids the server actually answered for \u2014\n * an id that came back without a row (deleted user) still counts as resolved, an id whose request\n * failed does not. Callers cache on `resolvedIds`, so a transient failure is retried rather than\n * remembered as a permanent blank.\n */\nexport type DeviceUserLookup = {\n options: DeviceUserOption[]\n resolvedIds: string[]\n}\n\n// Batch id \u2192 label resolution for rows already on screen, so a device whose owner never appeared in\n// a search result still renders a name instead of a bare UUID.\nexport async function resolveDeviceUserOptions(ids: string[], signal?: AbortSignal): Promise<DeviceUserLookup> {\n const unique = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)))\n if (unique.length === 0) return { options: [], resolvedIds: [] }\n const options: DeviceUserOption[] = []\n const resolvedIds: string[] = []\n for (let offset = 0; offset < unique.length; offset += MAX_IDS_PER_LOOKUP) {\n const batch = unique.slice(offset, offset + MAX_IDS_PER_LOOKUP)\n const params = new URLSearchParams()\n params.set('page', '1')\n params.set('pageSize', String(batch.length))\n // URLSearchParams encodes on toString(); pre-encoding here would double-escape the commas.\n params.set('ids', batch.join(','))\n const batchOptions = await fetchUsers(params, 'compact', signal)\n // One failed batch must not cost the batches that did answer.\n if (batchOptions === null) continue\n options.push(...batchOptions)\n resolvedIds.push(...batch)\n }\n return { options, resolvedIds }\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AACxB,SAAS,2BAA2B;AAapC,MAAM,mBAAmB;AAGzB,MAAM,qBAAqB;AAU3B,SAAS,SAAS,MAAuC,OAAuC;AAC9F,MAAI,CAAC,QAAQ,OAAO,KAAK,OAAO,YAAY,CAAC,KAAK,GAAG,KAAK,EAAG,QAAO,CAAC;AACrE,QAAM,KAAK,KAAK,GAAG,KAAK;AACxB,QAAM,OAAO,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI;AACpF,QAAM,QAAQ,OAAO,KAAK,UAAU,YAAY,KAAK,MAAM,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI;AACxF,QAAM,QAAQ,UAAU,YAAY,QAAQ,QAAQ,GAAG,IAAI,WAAM,KAAK,KAAK,QAAQ,SAAS;AAC5F,SAAO,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC;AAC9B;AAQA,eAAe,WACb,QACA,OACA,QACoC;AACpC,QAAM,OAAO,MAAM;AAAA,IACjB,mBAAmB,OAAO,SAAS,CAAC;AAAA,IACpC,EAAE,SAAS,EAAE,2BAA2B,IAAI,GAAG,OAAO;AAAA,IACtD,EAAE,UAAU,KAAK;AAAA,EACnB,EAAE,MAAM,MAAM,IAAI;AAClB,MAAI,CAAC,QAAQ,CAAC,KAAK,GAAI,QAAO;AAC9B,UAAQ,KAAK,QAAQ,SAAS,CAAC,GAAG,QAAQ,CAAC,SAAS,SAAS,MAAM,KAAK,CAAC;AAC3E;AAEA,eAAsB,sBAAsB,OAA6C;AACvF,QAAM,SAAS,IAAI,gBAAgB;AACnC,SAAO,IAAI,QAAQ,GAAG;AACtB,SAAO,IAAI,YAAY,OAAO,gBAAgB,CAAC;AAC/C,QAAM,UAAU,OAAO,KAAK;AAC5B,MAAI,QAAS,QAAO,IAAI,UAAU,OAAO;AAEzC,SAAQ,MAAM,WAAW,QAAQ,QAAQ,KAAM,CAAC;AAClD;AAeA,eAAsB,yBAAyB,KAAe,QAAiD;AAC7G,QAAM,SAAS,MAAM,KAAK,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,OAAO,OAAO,CAAC,CAAC;AAC7E,MAAI,OAAO,WAAW,EAAG,QAAO,EAAE,SAAS,CAAC,GAAG,aAAa,CAAC,EAAE;AAC/D,QAAM,UAA8B,CAAC;AACrC,QAAM,cAAwB,CAAC;AAC/B,WAAS,SAAS,GAAG,SAAS,OAAO,QAAQ,UAAU,oBAAoB;AACzE,UAAM,QAAQ,OAAO,MAAM,QAAQ,SAAS,kBAAkB;AAC9D,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,QAAQ,GAAG;AACtB,WAAO,IAAI,YAAY,OAAO,MAAM,MAAM,CAAC;AAE3C,WAAO,IAAI,OAAO,MAAM,KAAK,GAAG,CAAC;AACjC,UAAM,eAAe,MAAM,WAAW,QAAQ,WAAW,MAAM;AAE/D,QAAI,iBAAiB,KAAM;AAC3B,YAAQ,KAAK,GAAG,YAAY;AAC5B,gBAAY,KAAK,GAAG,KAAK;AAAA,EAC3B;AACA,SAAO,EAAE,SAAS,YAAY;AAChC;",
6
+ "names": []
7
+ }
@@ -1,7 +1,11 @@
1
1
  const setup = {
2
2
  defaultRoleFeatures: {
3
- superadmin: ["devices.*"],
4
- admin: ["devices.*"],
3
+ // `auth.users.list` is granted alongside `devices.admin` rather than left to the auth module's
4
+ // own `admin: ['auth.*']`, so the dependency declared in `acl.ts` holds even where that grant
5
+ // was narrowed. Without it the owner picker has nothing to offer and the register form cannot
6
+ // be completed. Existing tenants pick this up via `yarn mercato auth sync-role-acls`.
7
+ superadmin: ["devices.*", "auth.users.list"],
8
+ admin: ["devices.*", "auth.users.list"],
5
9
  employee: ["devices.view", "devices.manage"]
6
10
  }
7
11
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/devices/setup.ts"],
4
- "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n superadmin: ['devices.*'],\n admin: ['devices.*'],\n employee: ['devices.view', 'devices.manage'],\n },\n}\n\nexport default setup\n"],
5
- "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,YAAY,CAAC,WAAW;AAAA,IACxB,OAAO,CAAC,WAAW;AAAA,IACnB,UAAU,CAAC,gBAAgB,gBAAgB;AAAA,EAC7C;AACF;AAEA,IAAO,gBAAQ;",
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n // `auth.users.list` is granted alongside `devices.admin` rather than left to the auth module's\n // own `admin: ['auth.*']`, so the dependency declared in `acl.ts` holds even where that grant\n // was narrowed. Without it the owner picker has nothing to offer and the register form cannot\n // be completed. Existing tenants pick this up via `yarn mercato auth sync-role-acls`.\n superadmin: ['devices.*', 'auth.users.list'],\n admin: ['devices.*', 'auth.users.list'],\n employee: ['devices.view', 'devices.manage'],\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKnB,YAAY,CAAC,aAAa,iBAAiB;AAAA,IAC3C,OAAO,CAAC,aAAa,iBAAiB;AAAA,IACtC,UAAU,CAAC,gBAAgB,gBAAgB;AAAA,EAC7C;AACF;AAEA,IAAO,gBAAQ;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,21 @@
1
+ import { escapeLikePattern } from "@open-mercato/shared/lib/db/escapeLikePattern";
2
+ import { locales } from "@open-mercato/shared/lib/i18n/config";
3
+ import { matchCountryCodes } from "@open-mercato/shared/lib/location/countries";
4
+ function buildWarehouseListSearchOr(term) {
5
+ const like = `%${escapeLikePattern(term)}%`;
6
+ const orFilters = [
7
+ { name: { $ilike: like } },
8
+ { code: { $ilike: like } },
9
+ { city: { $ilike: like } },
10
+ { country: { $ilike: like } }
11
+ ];
12
+ const matchedCountryCodes = matchCountryCodes(term, { locales });
13
+ if (matchedCountryCodes.length > 0) {
14
+ orFilters.push({ country: { $in: matchedCountryCodes } });
15
+ }
16
+ return orFilters;
17
+ }
18
+ export {
19
+ buildWarehouseListSearchOr
20
+ };
21
+ //# sourceMappingURL=warehouseSearch.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/wms/api/warehouseSearch.ts"],
4
+ "sourcesContent": ["import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { locales } from '@open-mercato/shared/lib/i18n/config'\nimport { matchCountryCodes } from '@open-mercato/shared/lib/location/countries'\n\n/**\n * Warehouse list search matches name/code/city plus both stored country values:\n * ISO codes (`PL`) and legacy free-text (`Poland`). Localized labels shown in\n * the table (`Poland` / `Polska`) must resolve back to the stored ISO code.\n */\nexport function buildWarehouseListSearchOr(term: string): Array<Record<string, unknown>> {\n const like = `%${escapeLikePattern(term)}%`\n const orFilters: Array<Record<string, unknown>> = [\n { name: { $ilike: like } },\n { code: { $ilike: like } },\n { city: { $ilike: like } },\n { country: { $ilike: like } },\n ]\n const matchedCountryCodes = matchCountryCodes(term, { locales })\n if (matchedCountryCodes.length > 0) {\n orFilters.push({ country: { $in: matchedCountryCodes } })\n }\n return orFilters\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAyB;AAClC,SAAS,eAAe;AACxB,SAAS,yBAAyB;AAO3B,SAAS,2BAA2B,MAA8C;AACvF,QAAM,OAAO,IAAI,kBAAkB,IAAI,CAAC;AACxC,QAAM,YAA4C;AAAA,IAChD,EAAE,MAAM,EAAE,QAAQ,KAAK,EAAE;AAAA,IACzB,EAAE,MAAM,EAAE,QAAQ,KAAK,EAAE;AAAA,IACzB,EAAE,MAAM,EAAE,QAAQ,KAAK,EAAE;AAAA,IACzB,EAAE,SAAS,EAAE,QAAQ,KAAK,EAAE;AAAA,EAC9B;AACA,QAAM,sBAAsB,kBAAkB,MAAM,EAAE,QAAQ,CAAC;AAC/D,MAAI,oBAAoB,SAAS,GAAG;AAClC,cAAU,KAAK,EAAE,SAAS,EAAE,KAAK,oBAAoB,EAAE,CAAC;AAAA,EAC1D;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -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.js";
7
+ import { buildWarehouseListSearchOr } from "../warehouseSearch.js";
8
8
  import { Warehouse } from "../../data/entities.js";
9
9
  import { warehouseCreateSchema, warehouseUpdateSchema } from "../../data/validators.js";
10
10
  import { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from "../openapi.js";
@@ -71,6 +71,9 @@ const crud = makeCrudRoute({
71
71
  F.created_at,
72
72
  F.updated_at
73
73
  ],
74
+ decorateCustomFields: {
75
+ entityIds: E.wms.warehouse
76
+ },
74
77
  sortFieldMap: {
75
78
  name: F.name,
76
79
  code: F.code,
@@ -88,13 +91,7 @@ const crud = makeCrudRoute({
88
91
  if (isActive !== null) filters[F.is_active] = { $eq: isActive };
89
92
  const term = query.search?.trim();
90
93
  if (term) {
91
- const like = `%${escapeLikePattern(term)}%`;
92
- filters.$or = [
93
- { [F.name]: { $ilike: like } },
94
- { [F.code]: { $ilike: like } },
95
- { [F.city]: { $ilike: like } },
96
- { [F.country]: { $ilike: like } }
97
- ];
94
+ filters.$or = buildWarehouseListSearchOr(term);
98
95
  }
99
96
  return filters;
100
97
  }
@@ -107,7 +104,10 @@ const crud = makeCrudRoute({
107
104
  const { translate } = await resolveTranslations();
108
105
  return parseScopedCommandInput(warehouseCreateSchema, raw ?? {}, ctx, translate);
109
106
  },
110
- response: ({ result }) => ({ id: result?.warehouseId ?? null }),
107
+ response: ({ result }) => ({
108
+ id: result?.warehouseId ?? null,
109
+ updatedAt: result?.updatedAt ?? null
110
+ }),
111
111
  status: 201
112
112
  },
113
113
  update: {
@@ -157,6 +157,10 @@ const openApi = createWmsCrudOpenApi({
157
157
  listResponseSchema: createPagedListResponseSchema(warehouseListItemSchema),
158
158
  create: {
159
159
  schema: warehouseCreateSchema,
160
+ responseSchema: z.object({
161
+ id: z.string().uuid().nullable(),
162
+ updatedAt: z.string().nullable()
163
+ }),
160
164
  description: "Creates a warehouse for inventory operations."
161
165
  },
162
166
  update: {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/wms/api/warehouses/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '@open-mercato/shared/lib/api/scoped'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { E } from '#generated/entities.ids.generated'\nimport { Warehouse } from '../../data/entities'\nimport { warehouseCreateSchema, warehouseUpdateSchema } from '../../data/validators'\nimport { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from '../openapi'\n\nconst F = {\n id: 'id',\n organization_id: 'organization_id',\n tenant_id: 'tenant_id',\n name: 'name',\n code: 'code',\n is_active: 'is_active',\n is_primary: 'is_primary',\n address_line1: 'address_line1',\n city: 'city',\n postal_code: 'postal_code',\n country: 'country',\n timezone: 'timezone',\n created_at: 'created_at',\n updated_at: 'updated_at',\n} as const\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['wms.view'] },\n POST: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n PUT: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n DELETE: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n ids: z.string().optional(),\n isActive: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: Warehouse,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.wms.warehouse },\n list: {\n schema: listSchema,\n disableListCache: true,\n entityId: E.wms.warehouse,\n fields: [\n F.id,\n F.organization_id,\n F.tenant_id,\n F.name,\n F.code,\n F.is_active,\n F.is_primary,\n F.address_line1,\n F.city,\n F.postal_code,\n F.country,\n F.timezone,\n F.created_at,\n F.updated_at,\n ],\n sortFieldMap: {\n name: F.name,\n code: F.code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.ids === 'string' && query.ids.trim().length > 0) {\n filters[F.id] = {\n $in: query.ids.split(',').map((value) => value.trim()).filter((value) => value.length > 0),\n }\n }\n const isActive = parseBooleanToken(query.isActive)\n if (isActive !== null) filters[F.is_active] = { $eq: isActive }\n const term = query.search?.trim()\n if (term) {\n const like = `%${escapeLikePattern(term)}%`\n filters.$or = [\n { [F.name]: { $ilike: like } },\n { [F.code]: { $ilike: like } },\n { [F.city]: { $ilike: like } },\n { [F.country]: { $ilike: like } },\n ]\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'wms.warehouses.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.warehouseId ?? null }),\n status: 201,\n },\n update: {\n commandId: 'wms.warehouses.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'wms.warehouses.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n return { id: resolveCrudRecordId(parsed, ctx, translate) }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst warehouseListItemSchema = z.object({\n id: z.string().uuid().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n name: z.string().nullable().optional(),\n code: z.string().nullable().optional(),\n is_active: z.boolean().nullable().optional(),\n is_primary: z.boolean().nullable().optional(),\n address_line1: z.string().nullable().optional(),\n city: z.string().nullable().optional(),\n postal_code: z.string().nullable().optional(),\n country: z.string().nullable().optional(),\n timezone: z.string().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n})\n\nexport const openApi = createWmsCrudOpenApi({\n resourceName: 'Warehouse',\n pluralName: 'Warehouses',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(warehouseListItemSchema),\n create: {\n schema: warehouseCreateSchema,\n description: 'Creates a warehouse for inventory operations.',\n },\n update: {\n schema: warehouseUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a warehouse by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a warehouse by id.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { parseScopedCommandInput, resolveCrudRecordId } from '@open-mercato/shared/lib/api/scoped'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { E } from '#generated/entities.ids.generated'\nimport { buildWarehouseListSearchOr } from '../warehouseSearch'\nimport { Warehouse } from '../../data/entities'\nimport { warehouseCreateSchema, warehouseUpdateSchema } from '../../data/validators'\nimport { createPagedListResponseSchema, createWmsCrudOpenApi, defaultOkResponseSchema } from '../openapi'\n\nconst F = {\n id: 'id',\n organization_id: 'organization_id',\n tenant_id: 'tenant_id',\n name: 'name',\n code: 'code',\n is_active: 'is_active',\n is_primary: 'is_primary',\n address_line1: 'address_line1',\n city: 'city',\n postal_code: 'postal_code',\n country: 'country',\n timezone: 'timezone',\n created_at: 'created_at',\n updated_at: 'updated_at',\n} as const\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['wms.view'] },\n POST: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n PUT: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n DELETE: { requireAuth: true, requireFeatures: ['wms.manage_warehouses'] },\n}\n\nexport const metadata = routeMetadata\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst listSchema = z.object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(25),\n search: z.string().optional(),\n ids: z.string().optional(),\n isActive: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n}).passthrough()\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: Warehouse,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.wms.warehouse },\n list: {\n schema: listSchema,\n disableListCache: true,\n entityId: E.wms.warehouse,\n fields: [\n F.id,\n F.organization_id,\n F.tenant_id,\n F.name,\n F.code,\n F.is_active,\n F.is_primary,\n F.address_line1,\n F.city,\n F.postal_code,\n F.country,\n F.timezone,\n F.created_at,\n F.updated_at,\n ],\n decorateCustomFields: {\n entityIds: E.wms.warehouse,\n },\n sortFieldMap: {\n name: F.name,\n code: F.code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => {\n const filters: Record<string, unknown> = {}\n if (typeof query.ids === 'string' && query.ids.trim().length > 0) {\n filters[F.id] = {\n $in: query.ids.split(',').map((value) => value.trim()).filter((value) => value.length > 0),\n }\n }\n const isActive = parseBooleanToken(query.isActive)\n if (isActive !== null) filters[F.is_active] = { $eq: isActive }\n const term = query.search?.trim()\n if (term) {\n filters.$or = buildWarehouseListSearchOr(term)\n }\n return filters\n },\n },\n actions: {\n create: {\n commandId: 'wms.warehouses.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({\n id: result?.warehouseId ?? null,\n updatedAt: result?.updatedAt ?? null,\n }),\n status: 201,\n },\n update: {\n commandId: 'wms.warehouses.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(warehouseUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'wms.warehouses.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n return { id: resolveCrudRecordId(parsed, ctx, translate) }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nconst warehouseListItemSchema = z.object({\n id: z.string().uuid().nullable().optional(),\n organization_id: z.string().uuid().nullable().optional(),\n tenant_id: z.string().uuid().nullable().optional(),\n name: z.string().nullable().optional(),\n code: z.string().nullable().optional(),\n is_active: z.boolean().nullable().optional(),\n is_primary: z.boolean().nullable().optional(),\n address_line1: z.string().nullable().optional(),\n city: z.string().nullable().optional(),\n postal_code: z.string().nullable().optional(),\n country: z.string().nullable().optional(),\n timezone: z.string().nullable().optional(),\n created_at: z.string().nullable().optional(),\n updated_at: z.string().nullable().optional(),\n})\n\nexport const openApi = createWmsCrudOpenApi({\n resourceName: 'Warehouse',\n pluralName: 'Warehouses',\n querySchema: listSchema,\n listResponseSchema: createPagedListResponseSchema(warehouseListItemSchema),\n create: {\n schema: warehouseCreateSchema,\n responseSchema: z.object({\n id: z.string().uuid().nullable(),\n updatedAt: z.string().nullable(),\n }),\n description: 'Creates a warehouse for inventory operations.',\n },\n update: {\n schema: warehouseUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates a warehouse by id.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes a warehouse by id.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,12 +1,12 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
- import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
3
2
  import {
4
3
  emitCrudSideEffects,
5
4
  emitCrudUndoSideEffects,
6
5
  parseWithCustomFields,
7
6
  setCustomFieldsIfAny
8
7
  } from "@open-mercato/shared/lib/commands/helpers";
9
- import { buildCustomFieldResetMap, loadCustomFieldSnapshot } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
8
+ import { loadCustomFieldSnapshot, buildCustomFieldResetMap } from "@open-mercato/shared/lib/commands/customFieldSnapshots";
9
+ import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
10
10
  import { CrudHttpError, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
11
11
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
12
12
  import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
@@ -37,6 +37,7 @@ import {
37
37
  normalizeOptionalString,
38
38
  requireId,
39
39
  toNumericString,
40
+ warehouseCrudIndexer,
40
41
  warehouseZoneCrudIndexer
41
42
  } from "./shared.js";
42
43
  import { emitWmsEvent } from "../events.js";
@@ -53,6 +54,17 @@ function toJsonValue(value) {
53
54
  if (value === void 0) return void 0;
54
55
  return value ?? null;
55
56
  }
57
+ async function persistWarehouseCustomFields(ctx, warehouse, custom) {
58
+ if (!custom || Object.keys(custom).length === 0) return;
59
+ await setCustomFieldsIfAny({
60
+ dataEngine: ctx.container.resolve("dataEngine"),
61
+ entityId: E.wms.warehouse,
62
+ recordId: warehouse.id,
63
+ organizationId: warehouse.organizationId,
64
+ tenantId: warehouse.tenantId,
65
+ values: custom
66
+ });
67
+ }
56
68
  async function buildCrudLog(ctx, input, resultId, actionKey, fallbackLabel, resourceKind) {
57
69
  const { translate } = await resolveTranslations();
58
70
  return {
@@ -131,7 +143,16 @@ function snapshotWarehouse(record) {
131
143
  }
132
144
  async function loadWarehouseSnapshot(em, ctx, id) {
133
145
  const record = await findOneWithDecryption(em, Warehouse, { id }, void 0, resolveScope(ctx));
134
- return record ? snapshotWarehouse(record) : null;
146
+ if (!record) return null;
147
+ const snapshot = snapshotWarehouse(record);
148
+ const custom = await loadCustomFieldSnapshot(em, {
149
+ entityId: E.wms.warehouse,
150
+ recordId: record.id,
151
+ tenantId: record.tenantId,
152
+ organizationId: record.organizationId
153
+ });
154
+ snapshot.custom = custom && Object.keys(custom).length ? custom : null;
155
+ return snapshot;
135
156
  }
136
157
  function snapshotWarehouseZone(record) {
137
158
  const warehouseId = typeof record.warehouse === "string" ? record.warehouse : record.warehouse.id;
@@ -171,6 +192,24 @@ async function writeZoneCustomFields(ctx, zone, values) {
171
192
  values
172
193
  });
173
194
  }
195
+ async function emitWarehouseCrudSideEffects(ctx, action, warehouse, origin = "write") {
196
+ const options = {
197
+ dataEngine: ctx.container.resolve("dataEngine"),
198
+ action,
199
+ entity: warehouse,
200
+ identifiers: {
201
+ id: warehouse.id,
202
+ organizationId: warehouse.organizationId,
203
+ tenantId: warehouse.tenantId
204
+ },
205
+ indexer: warehouseCrudIndexer
206
+ };
207
+ if (origin === "undo") {
208
+ await emitCrudUndoSideEffects(options);
209
+ return;
210
+ }
211
+ await emitCrudSideEffects(options);
212
+ }
174
213
  async function emitZoneCrudSideEffects(ctx, action, zone, origin = "write") {
175
214
  const options = {
176
215
  dataEngine: ctx.container.resolve("dataEngine"),
@@ -408,7 +447,7 @@ async function resolveParentLocation(em, ctx, warehouseId, parentId) {
408
447
  const createWarehouseCommand = {
409
448
  id: "wms.warehouses.create",
410
449
  async execute(rawInput, ctx) {
411
- const parsed = warehouseCreateSchema.parse(rawInput ?? {});
450
+ const { parsed, custom } = parseWithCustomFields(warehouseCreateSchema, rawInput ?? {});
412
451
  ensureTenantScope(ctx, parsed.tenantId);
413
452
  ensureOrganizationScope(ctx, parsed.organizationId);
414
453
  const em = resolveEm(ctx);
@@ -466,6 +505,8 @@ const createWarehouseCommand = {
466
505
  }
467
506
  throw err;
468
507
  }
508
+ await persistWarehouseCustomFields(ctx, warehouse, custom);
509
+ await emitWarehouseCrudSideEffects(ctx, "created", warehouse);
469
510
  void emitWmsEvent("wms.warehouse.created", {
470
511
  id: warehouse.id,
471
512
  warehouseId: warehouse.id,
@@ -474,6 +515,7 @@ const createWarehouseCommand = {
474
515
  }).catch(() => void 0);
475
516
  return {
476
517
  warehouseId: warehouse.id,
518
+ updatedAt: isoOrNull(warehouse.updatedAt),
477
519
  ...demotedPrimariesBefore.length > 0 ? { demotedPrimariesBefore } : {}
478
520
  };
479
521
  },
@@ -513,6 +555,8 @@ const createWarehouseCommand = {
513
555
  ensureOrganizationScope(ctx, record.organizationId);
514
556
  record.deletedAt = /* @__PURE__ */ new Date();
515
557
  await em.flush();
558
+ await persistWarehouseCustomFields(ctx, after, buildCustomFieldResetMap(void 0, after.custom ?? void 0));
559
+ await emitWarehouseCrudSideEffects(ctx, "deleted", record, "undo");
516
560
  if (payload.demotedPrimariesBefore?.length) {
517
561
  await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore);
518
562
  await em.flush();
@@ -528,7 +572,7 @@ const updateWarehouseCommand = {
528
572
  return before ? { before } : {};
529
573
  },
530
574
  async execute(rawInput, ctx) {
531
- const parsed = warehouseUpdateSchema.parse(rawInput ?? {});
575
+ const { parsed, custom } = parseWithCustomFields(warehouseUpdateSchema, rawInput ?? {});
532
576
  const em = resolveEm(ctx);
533
577
  const warehouse = await loadWarehouse(em, ctx, parsed.id);
534
578
  if (parsed.code !== void 0 && parsed.code !== warehouse.code) {
@@ -581,6 +625,8 @@ const updateWarehouseCommand = {
581
625
  }
582
626
  throw err;
583
627
  }
628
+ await persistWarehouseCustomFields(ctx, warehouse, custom);
629
+ await emitWarehouseCrudSideEffects(ctx, "updated", warehouse);
584
630
  void emitWmsEvent("wms.warehouse.updated", {
585
631
  id: warehouse.id,
586
632
  warehouseId: warehouse.id,
@@ -661,6 +707,12 @@ const updateWarehouseCommand = {
661
707
  record.deletedAt = null;
662
708
  }
663
709
  await em.flush();
710
+ await persistWarehouseCustomFields(
711
+ ctx,
712
+ before,
713
+ buildCustomFieldResetMap(before.custom ?? void 0, payload.after?.custom ?? void 0)
714
+ );
715
+ await emitWarehouseCrudSideEffects(ctx, "updated", record, "undo");
664
716
  if (payload.demotedPrimariesBefore?.length) {
665
717
  await restoreDemotedPrimaryWarehouses(em, ctx, payload.demotedPrimariesBefore);
666
718
  await em.flush();
@@ -681,6 +733,7 @@ const deleteWarehouseCommand = {
681
733
  const warehouse = await loadWarehouse(em, ctx, warehouseId);
682
734
  warehouse.deletedAt = /* @__PURE__ */ new Date();
683
735
  await em.flush();
736
+ await emitWarehouseCrudSideEffects(ctx, "deleted", warehouse);
684
737
  return { warehouseId: warehouse.id };
685
738
  },
686
739
  buildLog: async ({ input, result, ctx, snapshots }) => {
@@ -735,6 +788,7 @@ const deleteWarehouseCommand = {
735
788
  record.metadata = before.metadata;
736
789
  }
737
790
  await em.flush();
791
+ await emitWarehouseCrudSideEffects(ctx, "created", record, "undo");
738
792
  }
739
793
  };
740
794
  async function restoreZoneFromSnapshot(em, before) {