@open-mercato/core 0.6.8-develop.7078.1.ca2d1510b5 → 0.6.8-develop.7080.1.9b0d558a3f

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 (38) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/devices/api/admin/devices/[id]/route.js +5 -32
  3. package/dist/modules/devices/api/admin/devices/[id]/route.js.map +2 -2
  4. package/dist/modules/devices/api/admin/devices/route.js +14 -1
  5. package/dist/modules/devices/api/admin/devices/route.js.map +2 -2
  6. package/dist/modules/devices/api/deviceList.js +99 -12
  7. package/dist/modules/devices/api/deviceList.js.map +2 -2
  8. package/dist/modules/devices/api/deviceSerialization.js +46 -0
  9. package/dist/modules/devices/api/deviceSerialization.js.map +7 -0
  10. package/dist/modules/devices/api/openapi.js +3 -1
  11. package/dist/modules/devices/api/openapi.js.map +2 -2
  12. package/dist/modules/devices/api/route.js +9 -1
  13. package/dist/modules/devices/api/route.js.map +2 -2
  14. package/dist/modules/devices/backend/devices/[id]/page.js +8 -8
  15. package/dist/modules/devices/backend/devices/[id]/page.js.map +2 -2
  16. package/dist/modules/devices/backend/devices/page.js +12 -12
  17. package/dist/modules/devices/backend/devices/page.js.map +2 -2
  18. package/dist/modules/push_notifications/backend/push_notifications/send/page.js +1 -1
  19. package/dist/modules/push_notifications/backend/push_notifications/send/page.js.map +2 -2
  20. package/dist/modules/sales/commands/documents.js +79 -1
  21. package/dist/modules/sales/commands/documents.js.map +2 -2
  22. package/package.json +7 -7
  23. package/src/modules/devices/AGENTS.md +8 -0
  24. package/src/modules/devices/api/admin/devices/[id]/route.ts +5 -34
  25. package/src/modules/devices/api/admin/devices/route.ts +14 -1
  26. package/src/modules/devices/api/deviceList.ts +135 -11
  27. package/src/modules/devices/api/deviceSerialization.ts +50 -0
  28. package/src/modules/devices/api/openapi.ts +9 -1
  29. package/src/modules/devices/api/route.ts +9 -1
  30. package/src/modules/devices/backend/devices/[id]/page.tsx +15 -15
  31. package/src/modules/devices/backend/devices/page.tsx +20 -20
  32. package/src/modules/push_notifications/backend/push_notifications/send/page.tsx +4 -4
  33. package/src/modules/sales/commands/documents.ts +93 -0
  34. package/src/modules/sales/i18n/de.json +3 -0
  35. package/src/modules/sales/i18n/en.json +3 -0
  36. package/src/modules/sales/i18n/es.json +3 -0
  37. package/src/modules/sales/i18n/ko.json +3 -0
  38. package/src/modules/sales/i18n/pl.json +3 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/devices/api/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveLocaleFromAcceptLanguage } from '@open-mercato/shared/lib/i18n/locale'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\nimport { UserDevice } from '../data/entities'\nimport {\n registerDeviceSchema,\n registerDeviceCommandSchema,\n type RegisterDeviceCommandInput,\n} from '../data/validators'\nimport { resolveDeviceActorUserId } from './auth'\nimport { createDevicesCrudOpenApi, createPagedListResponseSchema } from './openapi'\nimport { deviceListSchema, deviceListFields, deviceListSortFieldMap, deviceListItemSchema } from './deviceList'\nimport { executeRegister, type DeviceMutationContext } from './deviceOps'\n\nconst logger = createLogger('devices')\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.view'] },\n POST: { requireAuth: true, requireFeatures: ['devices.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst NO_MATCH_USER_ID = '00000000-0000-0000-0000-000000000000'\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: UserDevice,\n idField: 'id',\n // organization_id stays nullable, but scoping follows the standard org-scoped pattern (like every\n // other module): the factory auto-filters by the caller's organization scope. The query engine's\n // org scope is null-aware \u2014 callers whose scope includes null (or who have no org restriction) also\n // see null-org rows; a caller pinned to a specific org does not.\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.devices.user_device },\n // Align the CRUD cache resource tag with the command's resourceKind ('devices.user_device') so\n // writes through the device commands invalidate this list cache. Without this, the factory falls\n // back to the ORM entity name and the cache is never busted (stale list under ENABLE_CRUD_API_CACHE).\n // The factory does not own the write methods here (custom POST + command bus), so no events are emitted.\n events: { module: 'devices', entity: 'user_device' },\n list: {\n schema: deviceListSchema,\n entityId: E.devices.user_device,\n // push_token is a secret and is never exposed via the list API.\n fields: deviceListFields,\n sortFieldMap: deviceListSortFieldMap,\n // Exports are off for the self-serve list. The factory enables them for every list that does not\n // declare `export` (resolveAvailableExportFormats falls back to csv/json/xml/markdown), and its\n // full-export branch replaces the filters with `{}` instead of calling `buildFilters`\n // (`baseFilters = exportFullRequested ? {} : buildFilters(...)`). On this route `buildFilters` is\n // the only user-level boundary \u2014 automatic scoping covers tenant and organization, not the acting\n // user \u2014 so `?format=json&exportScope=full` would return every device in the caller's org scope.\n // With no available formats, `exportRequested` is never true and that branch is unreachable. Bulk\n // device data belongs to the admin tree (api/admin/devices), which gates cross-user listing.\n export: { enabled: false },\n // Self-serve: always scoped to the acting user. Cross-user listing lives in api/admin/devices.\n buildFilters: async (query, ctx) => {\n // Consume the client-supplied advanced filter here (this deletes every `filter[...]` key from\n // `query`) so the CRUD factory's own merge step becomes a no-op. That step is\n // `{ ...existingFilters, ...advancedWhere }` (advanced-filter-integration.ts), and condition\n // field names are never validated \u2014 `filter[conditions][0][field]=user_id` or `=$and` would\n // otherwise overwrite whatever key this route uses for its scope and widen the listing to other\n // users. `mergeAdvancedFilterTree` instead AND-combines under `$and: [routeFilters, treeWhere]`,\n // where the scope sits in its own array element and no client key can reach it. Same pattern as\n // customers/people, companies and deals.\n const advancedFilterTree = consumeAdvancedFilterState(query)\n const filters: Record<string, unknown> = {}\n const actorUserId = resolveDeviceActorUserId(ctx.auth)\n // Self-serve visibility is strictly the acting user's own devices; cross-user listing lives in\n // api/admin/devices. Fail closed when there is no resolvable acting user (e.g. API keys).\n filters.user_id = { $eq: actorUserId ?? NO_MATCH_USER_ID }\n if (query.platform) filters.platform = { $eq: query.platform }\n return mergeAdvancedFilterTree(filters, advancedFilterTree)\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const body = registerDeviceSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n // When the app doesn't send an explicit per-device locale, fall back to the request's\n // Accept-Language (the mobile client calls this with its own headers). Only on self-register \u2014\n // PUT stays tri-state, and admin register uses the admin's browser headers, not the device's.\n const headerLocale = resolveLocaleFromAcceptLanguage(req.headers.get('accept-language'))\n\n const commandInput = registerDeviceCommandSchema.parse({\n ...body,\n locale: body.locale ?? headerLocale ?? undefined,\n tenantId: auth.tenantId,\n organizationId,\n userId: actorUserId,\n } satisfies RegisterDeviceCommandInput)\n\n const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }\n return await executeRegister(mctx, commandInput)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {\n return NextResponse.json(\n { error: translate('devices.errors.already_registered', 'This device is already registered') },\n { status: 409 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.POST failed', { err })\n return NextResponse.json(\n { error: translate('devices.errors.register_failed', 'Failed to register device') },\n { status: 500 },\n )\n }\n}\n\nconst registerResponseSchema = z.object({\n id: z.string().uuid(),\n deviceId: z.string(),\n revived: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({\n resourceName: 'Device',\n pluralName: 'Devices',\n querySchema: deviceListSchema,\n listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),\n create: {\n schema: registerDeviceSchema,\n responseSchema: registerResponseSchema,\n description:\n 'Registers (or idempotently upserts) the current user\\'s device for the given deviceId. May include an initial push token.',\n },\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { consumeAdvancedFilterState, mergeAdvancedFilterTree } from '@open-mercato/shared/lib/crud/advanced-filter-integration'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveLocaleFromAcceptLanguage } from '@open-mercato/shared/lib/i18n/locale'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\nimport { UserDevice } from '../data/entities'\nimport {\n registerDeviceSchema,\n registerDeviceCommandSchema,\n type RegisterDeviceCommandInput,\n} from '../data/validators'\nimport { resolveDeviceActorUserId } from './auth'\nimport { createDevicesCrudOpenApi, createPagedListResponseSchema } from './openapi'\nimport {\n deviceListSchema,\n deviceListFields,\n deviceListSortFieldMap,\n deviceListItemSchema,\n transformDeviceListItem,\n} from './deviceList'\nimport { executeRegister, type DeviceMutationContext } from './deviceOps'\n\nconst logger = createLogger('devices')\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['devices.view'] },\n POST: { requireAuth: true, requireFeatures: ['devices.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst NO_MATCH_USER_ID = '00000000-0000-0000-0000-000000000000'\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: UserDevice,\n idField: 'id',\n // organization_id stays nullable, but scoping follows the standard org-scoped pattern (like every\n // other module): the factory auto-filters by the caller's organization scope. The query engine's\n // org scope is null-aware \u2014 callers whose scope includes null (or who have no org restriction) also\n // see null-org rows; a caller pinned to a specific org does not.\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.devices.user_device },\n // Align the CRUD cache resource tag with the command's resourceKind ('devices.user_device') so\n // writes through the device commands invalidate this list cache. Without this, the factory falls\n // back to the ORM entity name and the cache is never busted (stale list under ENABLE_CRUD_API_CACHE).\n // The factory does not own the write methods here (custom POST + command bus), so no events are emitted.\n events: { module: 'devices', entity: 'user_device' },\n list: {\n schema: deviceListSchema,\n entityId: E.devices.user_device,\n // push_token is a secret and is never exposed via the list API.\n fields: deviceListFields,\n sortFieldMap: deviceListSortFieldMap,\n // The query engine projects raw column names; expose them in camelCase like every other module.\n transformItem: transformDeviceListItem,\n // Exports are off for the self-serve list. The factory enables them for every list that does not\n // declare `export` (resolveAvailableExportFormats falls back to csv/json/xml/markdown), and its\n // full-export branch replaces the filters with `{}` instead of calling `buildFilters`\n // (`baseFilters = exportFullRequested ? {} : buildFilters(...)`). On this route `buildFilters` is\n // the only user-level boundary \u2014 automatic scoping covers tenant and organization, not the acting\n // user \u2014 so `?format=json&exportScope=full` would return every device in the caller's org scope.\n // With no available formats, `exportRequested` is never true and that branch is unreachable. Bulk\n // device data belongs to the admin tree (api/admin/devices), which gates cross-user listing.\n export: { enabled: false },\n // Self-serve: always scoped to the acting user. Cross-user listing lives in api/admin/devices.\n buildFilters: async (query, ctx) => {\n // Consume the client-supplied advanced filter here (this deletes every `filter[...]` key from\n // `query`) so the CRUD factory's own merge step becomes a no-op. That step is\n // `{ ...existingFilters, ...advancedWhere }` (advanced-filter-integration.ts), and condition\n // field names are never validated \u2014 `filter[conditions][0][field]=user_id` or `=$and` would\n // otherwise overwrite whatever key this route uses for its scope and widen the listing to other\n // users. `mergeAdvancedFilterTree` instead AND-combines under `$and: [routeFilters, treeWhere]`,\n // where the scope sits in its own array element and no client key can reach it. Same pattern as\n // customers/people, companies and deals.\n const advancedFilterTree = consumeAdvancedFilterState(query)\n const filters: Record<string, unknown> = {}\n const actorUserId = resolveDeviceActorUserId(ctx.auth)\n // Self-serve visibility is strictly the acting user's own devices; cross-user listing lives in\n // api/admin/devices. Fail closed when there is no resolvable acting user (e.g. API keys).\n filters.user_id = { $eq: actorUserId ?? NO_MATCH_USER_ID }\n if (query.platform) filters.platform = { $eq: query.platform }\n return mergeAdvancedFilterTree(filters, advancedFilterTree)\n },\n },\n})\n\nexport const GET = crud.GET\n\nexport async function POST(req: Request) {\n const { translate } = await resolveTranslations()\n try {\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const actorUserId = resolveDeviceActorUserId(auth)\n if (!auth || !auth.tenantId || !actorUserId) {\n return NextResponse.json({ error: translate('devices.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n\n const body = registerDeviceSchema.parse(await readJsonSafe(req, {}))\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationId = scope?.selectedId ?? auth.orgId ?? null\n\n // When the app doesn't send an explicit per-device locale, fall back to the request's\n // Accept-Language (the mobile client calls this with its own headers). Only on self-register \u2014\n // PUT stays tri-state, and admin register uses the admin's browser headers, not the device's.\n const headerLocale = resolveLocaleFromAcceptLanguage(req.headers.get('accept-language'))\n\n const commandInput = registerDeviceCommandSchema.parse({\n ...body,\n locale: body.locale ?? headerLocale ?? undefined,\n tenantId: auth.tenantId,\n organizationId,\n userId: actorUserId,\n } satisfies RegisterDeviceCommandInput)\n\n const mctx: DeviceMutationContext = { container, auth, scope, organizationId, actorUserId, request: req }\n return await executeRegister(mctx, commandInput)\n } catch (err) {\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: translate('devices.errors.invalid_payload', 'Invalid device payload'), details: err.flatten() },\n { status: 400 },\n )\n }\n if (isCrudHttpError(err) && (err.body as { code?: string } | undefined)?.code === 'device_already_registered') {\n return NextResponse.json(\n { error: translate('devices.errors.already_registered', 'This device is already registered') },\n { status: 409 },\n )\n }\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('devices.POST failed', { err })\n return NextResponse.json(\n { error: translate('devices.errors.register_failed', 'Failed to register device') },\n { status: 500 },\n )\n }\n}\n\nconst registerResponseSchema = z.object({\n id: z.string().uuid(),\n deviceId: z.string(),\n revived: z.boolean(),\n})\n\nexport const openApi: OpenApiRouteDoc = createDevicesCrudOpenApi({\n resourceName: 'Device',\n pluralName: 'Devices',\n querySchema: deviceListSchema,\n listResponseSchema: createPagedListResponseSchema(deviceListItemSchema),\n create: {\n schema: registerDeviceSchema,\n responseSchema: registerResponseSchema,\n description:\n 'Registers (or idempotently upserts) the current user\\'s device for the given deviceId. May include an initial push token.',\n },\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -46,7 +46,7 @@ function DeviceAdminEditPage({ params }) {
46
46
  };
47
47
  }, [id, t]);
48
48
  React.useEffect(() => {
49
- const userId = device?.user_id;
49
+ const userId = device?.userId;
50
50
  if (!userId) return;
51
51
  let cancelled = false;
52
52
  void (async () => {
@@ -63,7 +63,7 @@ function DeviceAdminEditPage({ params }) {
63
63
  return () => {
64
64
  cancelled = true;
65
65
  };
66
- }, [device?.user_id]);
66
+ }, [device?.userId]);
67
67
  const fields = React.useMemo(() => [
68
68
  { id: "clientAppVersion", label: t("devices.form.appVersion"), type: "text" },
69
69
  { id: "osVersion", label: t("devices.form.osVersion"), type: "text" },
@@ -96,7 +96,7 @@ function DeviceAdminEditPage({ params }) {
96
96
  contentHeader: /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-1 gap-3 rounded-md border bg-muted p-4 text-sm sm:grid-cols-3", children: [
97
97
  /* @__PURE__ */ jsxs("div", { children: [
98
98
  /* @__PURE__ */ jsx("dt", { className: "text-xs font-medium text-muted-foreground", children: t("devices.form.deviceId") }),
99
- /* @__PURE__ */ jsx("dd", { className: "mt-1", children: /* @__PURE__ */ jsx("code", { className: "text-xs", children: device.device_id }) })
99
+ /* @__PURE__ */ jsx("dd", { className: "mt-1", children: /* @__PURE__ */ jsx("code", { className: "text-xs", children: device.deviceId }) })
100
100
  ] }),
101
101
  /* @__PURE__ */ jsxs("div", { children: [
102
102
  /* @__PURE__ */ jsx("dt", { className: "text-xs font-medium text-muted-foreground", children: t("devices.form.platform") }),
@@ -104,16 +104,16 @@ function DeviceAdminEditPage({ params }) {
104
104
  ] }),
105
105
  /* @__PURE__ */ jsxs("div", { children: [
106
106
  /* @__PURE__ */ jsx("dt", { className: "text-xs font-medium text-muted-foreground", children: t("devices.form.userId") }),
107
- /* @__PURE__ */ jsx("dd", { className: "mt-1", children: userLabel ? /* @__PURE__ */ jsx(Link, { href: `/backend/users/${encodeURIComponent(device.user_id)}/edit`, className: "text-primary hover:underline", children: userLabel }) : /* @__PURE__ */ jsx("code", { className: "text-xs", children: device.user_id }) })
107
+ /* @__PURE__ */ jsx("dd", { className: "mt-1", children: userLabel ? /* @__PURE__ */ jsx(Link, { href: `/backend/users/${encodeURIComponent(device.userId)}/edit`, className: "text-primary hover:underline", children: userLabel }) : /* @__PURE__ */ jsx("code", { className: "text-xs", children: device.userId }) })
108
108
  ] })
109
109
  ] }),
110
110
  fields,
111
111
  groups,
112
- optimisticLockUpdatedAt: device.updated_at,
112
+ optimisticLockUpdatedAt: device.updatedAt,
113
113
  initialValues: {
114
- clientAppVersion: device.client_app_version ?? "",
115
- osVersion: device.os_version ?? "",
116
- pushProvider: device.push_provider ?? ""
114
+ clientAppVersion: device.clientAppVersion ?? "",
115
+ osVersion: device.osVersion ?? "",
116
+ pushProvider: device.pushProvider ?? ""
117
117
  },
118
118
  submitLabel: t("common.save"),
119
119
  cancelHref: "/backend/devices",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/devices/backend/devices/%5Bid%5D/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype DeviceDetail = {\n id: string\n user_id: string\n device_id: string\n platform: string\n client_app_version: string | null\n os_version: string | null\n push_provider: string | null\n updated_at: string | null\n}\n\ntype FormValues = {\n clientAppVersion: string\n osVersion: string\n pushProvider: string\n}\n\nexport default function DeviceAdminEditPage({ params }: { params?: { id?: string } }) {\n const router = useRouter()\n const t = useT()\n const id = typeof params?.id === 'string' ? params.id : ''\n const [device, setDevice] = React.useState<DeviceDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n const [userLabel, setUserLabel] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n setNotFound(false)\n const call = await apiCall<{ item?: DeviceDetail }>(\n `/api/devices/admin/devices/${encodeURIComponent(id)}`,\n undefined,\n { fallback: null },\n )\n if (cancelled) return\n if (call.ok && call.result?.item) {\n setDevice(call.result.item)\n } else if (call.status === 404 || (call.ok && !call.result?.item)) {\n setNotFound(true)\n } else {\n setError(t('devices.form.error.loadFailed'))\n }\n setIsLoading(false)\n }\n if (id) load()\n return () => { cancelled = true }\n }, [id, t])\n\n // Resolve the owner's display name for a link to their profile. Devices admins may not hold\n // auth.users.list, so fall back to the raw id (rendered without a link) instead of redirecting.\n React.useEffect(() => {\n const userId = device?.user_id\n if (!userId) return\n let cancelled = false\n void (async () => {\n const call = await apiCall<{ items?: { id: string; name?: string | null; email?: string | null }[] }>(\n `/api/auth/users?id=${encodeURIComponent(userId)}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n ).catch(() => null)\n if (cancelled || !call || !call.ok) return\n const found = call.result?.items?.find((u) => u.id === userId)\n const label = found?.name?.trim() || found?.email?.trim() || null\n if (label) setUserLabel(label)\n })()\n return () => { cancelled = true }\n }, [device?.user_id])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'clientAppVersion', label: t('devices.form.appVersion'), type: 'text' },\n { id: 'osVersion', label: t('devices.form.osVersion'), type: 'text' },\n { id: 'pushProvider', label: t('devices.form.pushProvider'), type: 'text' },\n ], [t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => ([\n { id: 'details', title: t('devices.form.details'), column: 1, fields: ['clientAppVersion', 'osVersion', 'pushProvider'] },\n ]), [t])\n\n if (isLoading) {\n return <Page><PageBody><LoadingMessage label={t('common.loading')} /></PageBody></Page>\n }\n if (notFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('devices.errors.not_found')}\n backHref=\"/backend/devices\"\n backLabel={t('devices.list.title')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !device) {\n return <Page><PageBody><ErrorMessage label={error ?? t('devices.form.error.loadFailed')} /></PageBody></Page>\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('devices.form.editTitle')}\n backHref=\"/backend/devices\"\n contentHeader={(\n <dl className=\"grid grid-cols-1 gap-3 rounded-md border bg-muted p-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.deviceId')}</dt>\n <dd className=\"mt-1\"><code className=\"text-xs\">{device.device_id}</code></dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.platform')}</dt>\n <dd className=\"mt-1\">{device.platform}</dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.userId')}</dt>\n <dd className=\"mt-1\">{userLabel ? (\n <Link href={`/backend/users/${encodeURIComponent(device.user_id)}/edit`} className=\"text-primary hover:underline\">{userLabel}</Link>\n ) : (\n <code className=\"text-xs\">{device.user_id}</code>\n )}</dd>\n </div>\n </dl>\n )}\n fields={fields}\n groups={groups}\n optimisticLockUpdatedAt={device.updated_at}\n initialValues={{\n clientAppVersion: device.client_app_version ?? '',\n osVersion: device.os_version ?? '',\n pushProvider: device.push_provider ?? '',\n }}\n submitLabel={t('common.save')}\n cancelHref=\"/backend/devices\"\n onSubmit={async (values) => {\n await updateCrud(`devices/admin/devices/${encodeURIComponent(id)}`, {\n clientAppVersion: values.clientAppVersion.trim() || null,\n osVersion: values.osVersion.trim() || null,\n pushProvider: values.pushProvider.trim() || null,\n })\n flash(t('devices.form.success.updated'), 'success')\n router.push('/backend/devices')\n }}\n />\n </PageBody>\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 { useRouter } from 'next/navigation'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { updateCrud } from '@open-mercato/ui/backend/utils/crud'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage, RecordNotFoundState } from '@open-mercato/ui/backend/detail'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype DeviceDetail = {\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 updatedAt: string | null\n}\n\ntype FormValues = {\n clientAppVersion: string\n osVersion: string\n pushProvider: string\n}\n\nexport default function DeviceAdminEditPage({ params }: { params?: { id?: string } }) {\n const router = useRouter()\n const t = useT()\n const id = typeof params?.id === 'string' ? params.id : ''\n const [device, setDevice] = React.useState<DeviceDetail | null>(null)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [notFound, setNotFound] = React.useState(false)\n const [userLabel, setUserLabel] = React.useState<string | null>(null)\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setIsLoading(true)\n setError(null)\n setNotFound(false)\n const call = await apiCall<{ item?: DeviceDetail }>(\n `/api/devices/admin/devices/${encodeURIComponent(id)}`,\n undefined,\n { fallback: null },\n )\n if (cancelled) return\n if (call.ok && call.result?.item) {\n setDevice(call.result.item)\n } else if (call.status === 404 || (call.ok && !call.result?.item)) {\n setNotFound(true)\n } else {\n setError(t('devices.form.error.loadFailed'))\n }\n setIsLoading(false)\n }\n if (id) load()\n return () => { cancelled = true }\n }, [id, t])\n\n // Resolve the owner's display name for a link to their profile. Devices admins may not hold\n // auth.users.list, so fall back to the raw id (rendered without a link) instead of redirecting.\n React.useEffect(() => {\n const userId = device?.userId\n if (!userId) return\n let cancelled = false\n void (async () => {\n const call = await apiCall<{ items?: { id: string; name?: string | null; email?: string | null }[] }>(\n `/api/auth/users?id=${encodeURIComponent(userId)}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n ).catch(() => null)\n if (cancelled || !call || !call.ok) return\n const found = call.result?.items?.find((u) => u.id === userId)\n const label = found?.name?.trim() || found?.email?.trim() || null\n if (label) setUserLabel(label)\n })()\n return () => { cancelled = true }\n }, [device?.userId])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'clientAppVersion', label: t('devices.form.appVersion'), type: 'text' },\n { id: 'osVersion', label: t('devices.form.osVersion'), type: 'text' },\n { id: 'pushProvider', label: t('devices.form.pushProvider'), type: 'text' },\n ], [t])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => ([\n { id: 'details', title: t('devices.form.details'), column: 1, fields: ['clientAppVersion', 'osVersion', 'pushProvider'] },\n ]), [t])\n\n if (isLoading) {\n return <Page><PageBody><LoadingMessage label={t('common.loading')} /></PageBody></Page>\n }\n if (notFound) {\n return (\n <Page>\n <PageBody>\n <RecordNotFoundState\n label={t('devices.errors.not_found')}\n backHref=\"/backend/devices\"\n backLabel={t('devices.list.title')}\n />\n </PageBody>\n </Page>\n )\n }\n if (error || !device) {\n return <Page><PageBody><ErrorMessage label={error ?? t('devices.form.error.loadFailed')} /></PageBody></Page>\n }\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('devices.form.editTitle')}\n backHref=\"/backend/devices\"\n contentHeader={(\n <dl className=\"grid grid-cols-1 gap-3 rounded-md border bg-muted p-4 text-sm sm:grid-cols-3\">\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.deviceId')}</dt>\n <dd className=\"mt-1\"><code className=\"text-xs\">{device.deviceId}</code></dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.platform')}</dt>\n <dd className=\"mt-1\">{device.platform}</dd>\n </div>\n <div>\n <dt className=\"text-xs font-medium text-muted-foreground\">{t('devices.form.userId')}</dt>\n <dd className=\"mt-1\">{userLabel ? (\n <Link href={`/backend/users/${encodeURIComponent(device.userId)}/edit`} className=\"text-primary hover:underline\">{userLabel}</Link>\n ) : (\n <code className=\"text-xs\">{device.userId}</code>\n )}</dd>\n </div>\n </dl>\n )}\n fields={fields}\n groups={groups}\n optimisticLockUpdatedAt={device.updatedAt}\n initialValues={{\n clientAppVersion: device.clientAppVersion ?? '',\n osVersion: device.osVersion ?? '',\n pushProvider: device.pushProvider ?? '',\n }}\n submitLabel={t('common.save')}\n cancelHref=\"/backend/devices\"\n onSubmit={async (values) => {\n await updateCrud(`devices/admin/devices/${encodeURIComponent(id)}`, {\n clientAppVersion: values.clientAppVersion.trim() || null,\n osVersion: values.osVersion.trim() || null,\n pushProvider: values.pushProvider.trim() || null,\n })\n flash(t('devices.form.success.updated'), 'success')\n router.push('/backend/devices')\n }}\n />\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -153,16 +153,16 @@ function DevicesAdminListPage() {
153
153
  }, [confirm, t]);
154
154
  const columns = React.useMemo(() => [
155
155
  {
156
- accessorKey: "device_id",
156
+ accessorKey: "deviceId",
157
157
  header: t("devices.list.columns.device"),
158
- cell: ({ row }) => /* @__PURE__ */ jsx("code", { className: "text-xs", children: row.original.device_id })
158
+ cell: ({ row }) => /* @__PURE__ */ jsx("code", { className: "text-xs", children: row.original.deviceId })
159
159
  },
160
160
  { accessorKey: "platform", header: t("devices.list.columns.platform") },
161
161
  {
162
- accessorKey: "user_id",
162
+ accessorKey: "userId",
163
163
  header: t("devices.list.columns.user"),
164
164
  cell: ({ row }) => {
165
- const userId = row.original.user_id;
165
+ const userId = row.original.userId;
166
166
  const label = userLabelById.get(userId);
167
167
  return (
168
168
  // Stop the click bubbling to the row, whose default action navigates to the device edit page.
@@ -179,24 +179,24 @@ function DevicesAdminListPage() {
179
179
  }
180
180
  },
181
181
  {
182
- accessorKey: "client_app_version",
182
+ accessorKey: "clientAppVersion",
183
183
  header: t("devices.list.columns.appVersion"),
184
- cell: ({ row }) => row.original.client_app_version || t("devices.list.noValue")
184
+ cell: ({ row }) => row.original.clientAppVersion || t("devices.list.noValue")
185
185
  },
186
186
  {
187
- accessorKey: "os_version",
187
+ accessorKey: "osVersion",
188
188
  header: t("devices.list.columns.osVersion"),
189
- cell: ({ row }) => row.original.os_version || t("devices.list.noValue")
189
+ cell: ({ row }) => row.original.osVersion || t("devices.list.noValue")
190
190
  },
191
191
  {
192
- accessorKey: "push_provider",
192
+ accessorKey: "pushProvider",
193
193
  header: t("devices.list.columns.pushProvider"),
194
- cell: ({ row }) => row.original.push_provider || t("devices.list.noValue")
194
+ cell: ({ row }) => row.original.pushProvider || t("devices.list.noValue")
195
195
  },
196
196
  {
197
- accessorKey: "last_seen_at",
197
+ accessorKey: "lastSeenAt",
198
198
  header: t("devices.list.columns.lastSeen"),
199
- cell: ({ row }) => formatDate(row.original.last_seen_at, t)
199
+ cell: ({ row }) => formatDate(row.original.lastSeenAt, t)
200
200
  }
201
201
  ], [t, userLabelById]);
202
202
  return /* @__PURE__ */ jsxs(Page, { children: [
@@ -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 user_id: string\n device_id: string\n platform: string\n client_app_version: string | null\n os_version: string | null\n push_provider: string | null\n push_token_updated_at: string | null\n last_seen_at: string | null\n created_at: 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: 'device_id',\n header: t('devices.list.columns.device'),\n cell: ({ row }) => <code className=\"text-xs\">{row.original.device_id}</code>,\n },\n { accessorKey: 'platform', header: t('devices.list.columns.platform') },\n {\n accessorKey: 'user_id',\n header: t('devices.list.columns.user'),\n cell: ({ row }) => {\n const userId = row.original.user_id\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: 'client_app_version',\n header: t('devices.list.columns.appVersion'),\n cell: ({ row }) => row.original.client_app_version || t('devices.list.noValue'),\n },\n {\n accessorKey: 'os_version',\n header: t('devices.list.columns.osVersion'),\n cell: ({ row }) => row.original.os_version || t('devices.list.noValue'),\n },\n {\n accessorKey: 'push_provider',\n header: t('devices.list.columns.pushProvider'),\n cell: ({ row }) => row.original.push_provider || t('devices.list.noValue'),\n },\n {\n accessorKey: 'last_seen_at',\n header: t('devices.list.columns.lastSeen'),\n cell: ({ row }) => formatDate(row.original.last_seen_at, 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'\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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",
6
6
  "names": []
7
7
  }
@@ -42,7 +42,7 @@ function DeviceField({ value, setValue, setFormValue, values, onState }) {
42
42
  ).catch(() => null).then((call) => {
43
43
  if (cancelled) return;
44
44
  const items = (call && call.ok ? call.result?.items : []) ?? [];
45
- const opts = items.filter((d) => !!d && typeof d.id === "string" && !!d.push_provider).map((d) => ({ id: d.id, label: `${d.device_id} \xB7 ${d.platform}${d.push_provider ? ` \xB7 ${d.push_provider}` : ""}`, platform: d.platform }));
45
+ const opts = items.filter((d) => !!d && typeof d.id === "string" && !!d.pushProvider).map((d) => ({ id: d.id, label: `${d.deviceId} \xB7 ${d.platform}${d.pushProvider ? ` \xB7 ${d.pushProvider}` : ""}`, platform: d.platform }));
46
46
  setDevices(opts);
47
47
  if (selected && !opts.some((o) => o.id === selected)) setValue("");
48
48
  }).finally(() => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/push_notifications/backend/push_notifications/send/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { Plus, X } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFieldOption, type CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@open-mercato/ui/primitives/select'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype KeyValue = { key: string; value: string }\n\ntype FormValues = {\n userId: string\n deviceId: string\n mode: string\n title: string\n body: string\n data: KeyValue[]\n sound: string\n badge: number | string\n image: string\n priority: string\n channelId: string\n // form-only helper (never sent): drives channelId visibility (hidden in silent / for an iOS target)\n showChannelId: boolean\n}\n\nconst DELIVERIES_HREF = '/backend/push_notifications'\nconst FORM_ID = 'push-custom-send-form'\n// Radix Select forbids an empty-string item value, so \"all devices\" uses a sentinel that we map\n// back to '' (the form value that means \"fan out to every push-capable device\").\nconst ALL_DEVICES = '__all__'\n\ntype DeviceOption = { id: string; label: string; platform: string }\n// Reported up to the page so the (custom) submit button can disable when there is nothing to send to.\ntype DeviceState = { userId: string; count: number; loading: boolean }\n\n// Dependent picker: loads the *selected recipient's* push-capable devices (admin devices API,\n// filtered by userId) so an admin can target one device or all of them. Reads the live form values,\n// so it re-loads whenever the recipient changes and clears a stale selection. It also derives\n// `showChannelId` (Android channel id is Android-only + visible-only) so the channelId field can hide\n// itself for a selected iOS device or in silent mode. Degrades to \"All devices\" when the admin lacks\n// devices.admin or the recipient has no push-capable device.\nfunction DeviceField({ value, setValue, setFormValue, values, onState }: CrudCustomFieldRenderProps & { onState?: (s: DeviceState) => void }) {\n const t = useT()\n const userId = typeof values?.userId === 'string' ? values.userId : ''\n const mode = typeof values?.mode === 'string' ? values.mode : 'visible'\n const selected = typeof value === 'string' && value ? value : ''\n const [devices, setDevices] = React.useState<DeviceOption[]>([])\n const [loading, setLoading] = React.useState(false)\n\n // Report the recipient + push-capable device count up so the page can disable \"Send\" when there is\n // nowhere to deliver (no recipient, or the recipient has zero push-capable devices).\n React.useEffect(() => {\n onState?.({ userId, count: devices.length, loading })\n }, [userId, devices.length, loading, onState])\n\n React.useEffect(() => {\n if (!userId) {\n setDevices([])\n if (selected) setValue('')\n return\n }\n let cancelled = false\n setLoading(true)\n const params = new URLSearchParams({ userId, pageSize: '50' })\n apiCall<{ items?: Array<{ id: string; device_id: string; platform: string; push_provider?: string | null }> }>(\n `/api/devices/admin/devices?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n )\n .catch(() => null)\n .then((call) => {\n if (cancelled) return\n const items = (call && call.ok ? call.result?.items : []) ?? []\n const opts = items\n .filter((d): d is { id: string; device_id: string; platform: string; push_provider?: string | null } =>\n !!d && typeof d.id === 'string' && !!d.push_provider)\n .map((d) => ({ id: d.id, label: `${d.device_id} \u00B7 ${d.platform}${d.push_provider ? ` \u00B7 ${d.push_provider}` : ''}`, platform: d.platform }))\n setDevices(opts)\n if (selected && !opts.some((o) => o.id === selected)) setValue('')\n })\n .finally(() => { if (!cancelled) setLoading(false) })\n return () => { cancelled = true }\n // Reload only when the recipient changes; `selected`/`setValue` are handled inside.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userId])\n\n // channelId is Android-only and irrelevant to silent pushes \u2192 hide it for a selected iOS device or\n // in silent mode. Only write when the derived value actually changes (avoids a render loop).\n const selectedPlatform = React.useMemo(() => devices.find((d) => d.id === selected)?.platform, [devices, selected])\n React.useEffect(() => {\n const show = mode !== 'silent' && selectedPlatform !== 'ios'\n if (values?.showChannelId !== show) setFormValue?.('showChannelId', show)\n }, [mode, selectedPlatform, values?.showChannelId, setFormValue])\n\n return (\n <div className=\"space-y-1\">\n <Select value={selected || ALL_DEVICES} onValueChange={(v) => setValue(v === ALL_DEVICES ? '' : v)} disabled={!userId}>\n <SelectTrigger>\n <SelectValue placeholder={`${t('push_notifications.send.deviceAll')} (${devices.length})`} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_DEVICES}>{`${t('push_notifications.send.deviceAll')} (${devices.length})`}</SelectItem>\n {devices.map((d) => (\n <SelectItem key={d.id} value={d.id}>{d.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className={`text-xs ${userId && !loading && devices.length === 0 ? 'text-status-warning-text' : 'text-muted-foreground'}`}>\n {!userId\n ? t('push_notifications.send.deviceSelectUserFirst')\n : loading\n ? t('common.loading', 'Loading\u2026')\n : devices.length === 0\n ? t('push_notifications.send.deviceNone')\n : t('push_notifications.send.deviceHint')}\n </p>\n </div>\n )\n}\n\n// Arbitrary key/value payload delivered inside the push `data` map (the app reads it). Shown for both\n// visible and silent sends \u2014 for silent it is the entire payload.\nfunction DataField({ value, setValue }: CrudCustomFieldRenderProps) {\n const t = useT()\n const pairs: KeyValue[] = Array.isArray(value) ? (value as KeyValue[]) : []\n const update = (next: KeyValue[]) => setValue(next)\n return (\n <div className=\"space-y-2\">\n {pairs.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">{t('push_notifications.send.dataEmpty')}</p>\n ) : null}\n {pairs.map((pair, index) => (\n <div key={index} className=\"flex items-center gap-2\">\n <Input\n placeholder={t('push_notifications.send.dataKey')}\n value={pair.key}\n onChange={(e) => update(pairs.map((p, i) => (i === index ? { ...p, key: e.target.value } : p)))}\n />\n <Input\n placeholder={t('push_notifications.send.dataValue')}\n value={pair.value}\n onChange={(e) => update(pairs.map((p, i) => (i === index ? { ...p, value: e.target.value } : p)))}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('push_notifications.send.dataRemove')}\n onClick={() => update(pairs.filter((_, i) => i !== index))}\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n </div>\n ))}\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => update([...pairs, { key: '', value: '' }])}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('push_notifications.send.dataAdd')}\n </Button>\n </div>\n )\n}\n\nexport default function PushCustomSendPage() {\n const router = useRouter()\n const t = useT()\n const [deviceState, setDeviceState] = React.useState<DeviceState>({ userId: '', count: 0, loading: false })\n const [submitting, setSubmitting] = React.useState(false)\n\n // Stable reporter for the device picker \u2192 bails when unchanged so it can't loop the render.\n const reportDeviceState = React.useCallback((next: DeviceState) => {\n setDeviceState((prev) =>\n prev.userId === next.userId && prev.count === next.count && prev.loading === next.loading ? prev : next)\n }, [])\n\n // Recipient search by name/email via /api/auth/users (mirrors the devices list + admin\n // notification-preferences picker). Return results only \u2014 the combobox retains them internally, so\n // pushing them into state that `fields` depends on would re-render the whole form per keystroke.\n const loadUserOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {\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 return (call.result?.items ?? []).flatMap((item): CrudFieldOption[] => {\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 ? `${name} \u2014 ${email}` : email ?? name ?? item.id\n return [{ value: item.id, label }]\n })\n }, [])\n\n const visibleOnly = { field: 'mode', equals: 'visible' } as const\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'userId', label: t('push_notifications.send.userId'), type: 'combobox', required: true, description: t('push_notifications.send.userIdHint'), loadOptions: loadUserOptions },\n { id: 'deviceId', label: t('push_notifications.send.device'), type: 'custom', component: (props) => <DeviceField {...props} onState={reportDeviceState} /> },\n {\n id: 'mode',\n label: t('push_notifications.send.mode'),\n type: 'select',\n required: true,\n description: t('push_notifications.send.modeHint'),\n options: [\n { value: 'visible', label: t('push_notifications.send.modeVisible') },\n { value: 'silent', label: t('push_notifications.send.modeSilent') },\n ],\n },\n { id: 'title', label: t('push_notifications.send.title'), type: 'text', required: true, visibleWhen: visibleOnly },\n { id: 'body', label: t('push_notifications.send.body'), type: 'textarea', visibleWhen: visibleOnly },\n { id: 'data', label: t('push_notifications.send.data'), type: 'custom', description: t('push_notifications.send.dataHint'), component: (props) => <DataField {...props} /> },\n { id: 'sound', label: t('push_notifications.send.sound'), type: 'text', visibleWhen: visibleOnly },\n { id: 'badge', label: t('push_notifications.send.badge'), type: 'number', visibleWhen: visibleOnly },\n { id: 'image', label: t('push_notifications.send.image'), type: 'text', visibleWhen: visibleOnly },\n {\n id: 'priority',\n label: t('push_notifications.send.priority'),\n type: 'select',\n options: [\n { value: '', label: t('push_notifications.send.priorityDefault') },\n { value: 'high', label: t('push_notifications.send.priorityHigh') },\n { value: 'normal', label: t('push_notifications.send.priorityNormal') },\n ],\n },\n { id: 'channelId', label: t('push_notifications.send.channelId'), type: 'text', visibleWhen: { field: 'showChannelId', equals: true } },\n ], [t, loadUserOptions, reportDeviceState])\n\n // NOTE: intentionally a flat field list (no `groups`). CrudForm only applies `visibleWhen` in the\n // ungrouped render path \u2014 grouped fields ignore it \u2014 and this form relies on mode/platform-driven\n // field visibility (silent hides title/body/options; iOS hides channelId).\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('push_notifications.send.pageTitle')}\n backHref={DELIVERIES_HREF}\n formId={FORM_ID}\n hideFooterActions\n fields={fields}\n initialValues={{ userId: '', deviceId: '', mode: 'visible', title: '', body: '', data: [], sound: '', badge: '', image: '', priority: '', channelId: '', showChannelId: true }}\n submitLabel={t('push_notifications.send.submit')}\n onSubmit={async (values) => {\n setSubmitting(true)\n try {\n const silent = values.mode === 'silent'\n const body = (values.body ?? '').trim()\n\n const dataMap: Record<string, string> = {}\n for (const pair of Array.isArray(values.data) ? values.data : []) {\n const key = (pair?.key ?? '').trim()\n if (key) dataMap[key] = `${pair?.value ?? ''}`\n }\n const hasData = Object.keys(dataMap).length > 0\n\n // Options that only affect a visible banner are dropped for silent.\n const pushOptions: Record<string, unknown> = {}\n if (!silent) {\n if (typeof values.sound === 'string' && values.sound.trim()) pushOptions.sound = values.sound.trim()\n const badgeNum = typeof values.badge === 'number' ? values.badge : Number.parseInt(String(values.badge ?? ''), 10)\n if (Number.isFinite(badgeNum) && badgeNum >= 0) pushOptions.badge = badgeNum\n if (typeof values.image === 'string' && values.image.trim()) pushOptions.image = values.image.trim()\n if (typeof values.channelId === 'string' && values.channelId.trim()) pushOptions.channelId = values.channelId.trim()\n }\n if (values.priority === 'high' || values.priority === 'normal') pushOptions.priority = values.priority\n const hasOptions = Object.keys(pushOptions).length > 0\n\n // Silent hides the title field; the API still needs one, so use a stable label for the log.\n const title = silent ? (values.title ?? '').trim() || 'Silent push' : (values.title ?? '').trim()\n\n await apiCallOrThrow('/api/push_notifications/custom-send', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n recipientUserId: (values.userId ?? '').trim(),\n deviceId: (values.deviceId ?? '').trim() || undefined,\n title,\n body: !silent && body.length > 0 ? body : undefined,\n silent,\n data: hasData ? dataMap : undefined,\n pushOptions: hasOptions ? pushOptions : undefined,\n }),\n })\n flash(t('push_notifications.send.success'), 'success')\n router.push(DELIVERIES_HREF)\n } finally {\n setSubmitting(false)\n }\n }}\n />\n <div className=\"mt-4 flex items-center justify-between gap-3\">\n <div className=\"text-xs\">\n {!deviceState.userId ? (\n <span className=\"text-muted-foreground\">{t('push_notifications.send.deviceSelectUserFirst')}</span>\n ) : deviceState.count === 0 && !deviceState.loading ? (\n <span className=\"text-status-warning-text\">{t('push_notifications.send.deviceNone')}</span>\n ) : null}\n </div>\n <div className=\"flex gap-2\">\n <Button asChild variant=\"outline\">\n <Link href={DELIVERIES_HREF}>{t('common.cancel', 'Cancel')}</Link>\n </Button>\n <Button\n type=\"submit\"\n form={FORM_ID}\n disabled={!deviceState.userId || deviceState.count === 0 || deviceState.loading || submitting}\n >\n {t('push_notifications.send.submit')}\n </Button>\n </div>\n </div>\n </PageBody>\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 { useRouter } from 'next/navigation'\nimport { Plus, X } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField, type CrudFieldOption, type CrudCustomFieldRenderProps } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@open-mercato/ui/primitives/select'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\ntype KeyValue = { key: string; value: string }\n\ntype FormValues = {\n userId: string\n deviceId: string\n mode: string\n title: string\n body: string\n data: KeyValue[]\n sound: string\n badge: number | string\n image: string\n priority: string\n channelId: string\n // form-only helper (never sent): drives channelId visibility (hidden in silent / for an iOS target)\n showChannelId: boolean\n}\n\nconst DELIVERIES_HREF = '/backend/push_notifications'\nconst FORM_ID = 'push-custom-send-form'\n// Radix Select forbids an empty-string item value, so \"all devices\" uses a sentinel that we map\n// back to '' (the form value that means \"fan out to every push-capable device\").\nconst ALL_DEVICES = '__all__'\n\ntype DeviceOption = { id: string; label: string; platform: string }\n// Reported up to the page so the (custom) submit button can disable when there is nothing to send to.\ntype DeviceState = { userId: string; count: number; loading: boolean }\n\n// Dependent picker: loads the *selected recipient's* push-capable devices (admin devices API,\n// filtered by userId) so an admin can target one device or all of them. Reads the live form values,\n// so it re-loads whenever the recipient changes and clears a stale selection. It also derives\n// `showChannelId` (Android channel id is Android-only + visible-only) so the channelId field can hide\n// itself for a selected iOS device or in silent mode. Degrades to \"All devices\" when the admin lacks\n// devices.admin or the recipient has no push-capable device.\nfunction DeviceField({ value, setValue, setFormValue, values, onState }: CrudCustomFieldRenderProps & { onState?: (s: DeviceState) => void }) {\n const t = useT()\n const userId = typeof values?.userId === 'string' ? values.userId : ''\n const mode = typeof values?.mode === 'string' ? values.mode : 'visible'\n const selected = typeof value === 'string' && value ? value : ''\n const [devices, setDevices] = React.useState<DeviceOption[]>([])\n const [loading, setLoading] = React.useState(false)\n\n // Report the recipient + push-capable device count up so the page can disable \"Send\" when there is\n // nowhere to deliver (no recipient, or the recipient has zero push-capable devices).\n React.useEffect(() => {\n onState?.({ userId, count: devices.length, loading })\n }, [userId, devices.length, loading, onState])\n\n React.useEffect(() => {\n if (!userId) {\n setDevices([])\n if (selected) setValue('')\n return\n }\n let cancelled = false\n setLoading(true)\n const params = new URLSearchParams({ userId, pageSize: '50' })\n apiCall<{ items?: Array<{ id: string; deviceId: string; platform: string; pushProvider?: string | null }> }>(\n `/api/devices/admin/devices?${params.toString()}`,\n { headers: { 'x-om-forbidden-redirect': '0' } },\n { fallback: null },\n )\n .catch(() => null)\n .then((call) => {\n if (cancelled) return\n const items = (call && call.ok ? call.result?.items : []) ?? []\n const opts = items\n .filter((d): d is { id: string; deviceId: string; platform: string; pushProvider?: string | null } =>\n !!d && typeof d.id === 'string' && !!d.pushProvider)\n .map((d) => ({ id: d.id, label: `${d.deviceId} \u00B7 ${d.platform}${d.pushProvider ? ` \u00B7 ${d.pushProvider}` : ''}`, platform: d.platform }))\n setDevices(opts)\n if (selected && !opts.some((o) => o.id === selected)) setValue('')\n })\n .finally(() => { if (!cancelled) setLoading(false) })\n return () => { cancelled = true }\n // Reload only when the recipient changes; `selected`/`setValue` are handled inside.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [userId])\n\n // channelId is Android-only and irrelevant to silent pushes \u2192 hide it for a selected iOS device or\n // in silent mode. Only write when the derived value actually changes (avoids a render loop).\n const selectedPlatform = React.useMemo(() => devices.find((d) => d.id === selected)?.platform, [devices, selected])\n React.useEffect(() => {\n const show = mode !== 'silent' && selectedPlatform !== 'ios'\n if (values?.showChannelId !== show) setFormValue?.('showChannelId', show)\n }, [mode, selectedPlatform, values?.showChannelId, setFormValue])\n\n return (\n <div className=\"space-y-1\">\n <Select value={selected || ALL_DEVICES} onValueChange={(v) => setValue(v === ALL_DEVICES ? '' : v)} disabled={!userId}>\n <SelectTrigger>\n <SelectValue placeholder={`${t('push_notifications.send.deviceAll')} (${devices.length})`} />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_DEVICES}>{`${t('push_notifications.send.deviceAll')} (${devices.length})`}</SelectItem>\n {devices.map((d) => (\n <SelectItem key={d.id} value={d.id}>{d.label}</SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className={`text-xs ${userId && !loading && devices.length === 0 ? 'text-status-warning-text' : 'text-muted-foreground'}`}>\n {!userId\n ? t('push_notifications.send.deviceSelectUserFirst')\n : loading\n ? t('common.loading', 'Loading\u2026')\n : devices.length === 0\n ? t('push_notifications.send.deviceNone')\n : t('push_notifications.send.deviceHint')}\n </p>\n </div>\n )\n}\n\n// Arbitrary key/value payload delivered inside the push `data` map (the app reads it). Shown for both\n// visible and silent sends \u2014 for silent it is the entire payload.\nfunction DataField({ value, setValue }: CrudCustomFieldRenderProps) {\n const t = useT()\n const pairs: KeyValue[] = Array.isArray(value) ? (value as KeyValue[]) : []\n const update = (next: KeyValue[]) => setValue(next)\n return (\n <div className=\"space-y-2\">\n {pairs.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">{t('push_notifications.send.dataEmpty')}</p>\n ) : null}\n {pairs.map((pair, index) => (\n <div key={index} className=\"flex items-center gap-2\">\n <Input\n placeholder={t('push_notifications.send.dataKey')}\n value={pair.key}\n onChange={(e) => update(pairs.map((p, i) => (i === index ? { ...p, key: e.target.value } : p)))}\n />\n <Input\n placeholder={t('push_notifications.send.dataValue')}\n value={pair.value}\n onChange={(e) => update(pairs.map((p, i) => (i === index ? { ...p, value: e.target.value } : p)))}\n />\n <IconButton\n type=\"button\"\n variant=\"ghost\"\n aria-label={t('push_notifications.send.dataRemove')}\n onClick={() => update(pairs.filter((_, i) => i !== index))}\n >\n <X className=\"h-4 w-4\" />\n </IconButton>\n </div>\n ))}\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => update([...pairs, { key: '', value: '' }])}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('push_notifications.send.dataAdd')}\n </Button>\n </div>\n )\n}\n\nexport default function PushCustomSendPage() {\n const router = useRouter()\n const t = useT()\n const [deviceState, setDeviceState] = React.useState<DeviceState>({ userId: '', count: 0, loading: false })\n const [submitting, setSubmitting] = React.useState(false)\n\n // Stable reporter for the device picker \u2192 bails when unchanged so it can't loop the render.\n const reportDeviceState = React.useCallback((next: DeviceState) => {\n setDeviceState((prev) =>\n prev.userId === next.userId && prev.count === next.count && prev.loading === next.loading ? prev : next)\n }, [])\n\n // Recipient search by name/email via /api/auth/users (mirrors the devices list + admin\n // notification-preferences picker). Return results only \u2014 the combobox retains them internally, so\n // pushing them into state that `fields` depends on would re-render the whole form per keystroke.\n const loadUserOptions = React.useCallback(async (query?: string): Promise<CrudFieldOption[]> => {\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 return (call.result?.items ?? []).flatMap((item): CrudFieldOption[] => {\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 ? `${name} \u2014 ${email}` : email ?? name ?? item.id\n return [{ value: item.id, label }]\n })\n }, [])\n\n const visibleOnly = { field: 'mode', equals: 'visible' } as const\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'userId', label: t('push_notifications.send.userId'), type: 'combobox', required: true, description: t('push_notifications.send.userIdHint'), loadOptions: loadUserOptions },\n { id: 'deviceId', label: t('push_notifications.send.device'), type: 'custom', component: (props) => <DeviceField {...props} onState={reportDeviceState} /> },\n {\n id: 'mode',\n label: t('push_notifications.send.mode'),\n type: 'select',\n required: true,\n description: t('push_notifications.send.modeHint'),\n options: [\n { value: 'visible', label: t('push_notifications.send.modeVisible') },\n { value: 'silent', label: t('push_notifications.send.modeSilent') },\n ],\n },\n { id: 'title', label: t('push_notifications.send.title'), type: 'text', required: true, visibleWhen: visibleOnly },\n { id: 'body', label: t('push_notifications.send.body'), type: 'textarea', visibleWhen: visibleOnly },\n { id: 'data', label: t('push_notifications.send.data'), type: 'custom', description: t('push_notifications.send.dataHint'), component: (props) => <DataField {...props} /> },\n { id: 'sound', label: t('push_notifications.send.sound'), type: 'text', visibleWhen: visibleOnly },\n { id: 'badge', label: t('push_notifications.send.badge'), type: 'number', visibleWhen: visibleOnly },\n { id: 'image', label: t('push_notifications.send.image'), type: 'text', visibleWhen: visibleOnly },\n {\n id: 'priority',\n label: t('push_notifications.send.priority'),\n type: 'select',\n options: [\n { value: '', label: t('push_notifications.send.priorityDefault') },\n { value: 'high', label: t('push_notifications.send.priorityHigh') },\n { value: 'normal', label: t('push_notifications.send.priorityNormal') },\n ],\n },\n { id: 'channelId', label: t('push_notifications.send.channelId'), type: 'text', visibleWhen: { field: 'showChannelId', equals: true } },\n ], [t, loadUserOptions, reportDeviceState])\n\n // NOTE: intentionally a flat field list (no `groups`). CrudForm only applies `visibleWhen` in the\n // ungrouped render path \u2014 grouped fields ignore it \u2014 and this form relies on mode/platform-driven\n // field visibility (silent hides title/body/options; iOS hides channelId).\n\n return (\n <Page>\n <PageBody>\n <CrudForm<FormValues>\n title={t('push_notifications.send.pageTitle')}\n backHref={DELIVERIES_HREF}\n formId={FORM_ID}\n hideFooterActions\n fields={fields}\n initialValues={{ userId: '', deviceId: '', mode: 'visible', title: '', body: '', data: [], sound: '', badge: '', image: '', priority: '', channelId: '', showChannelId: true }}\n submitLabel={t('push_notifications.send.submit')}\n onSubmit={async (values) => {\n setSubmitting(true)\n try {\n const silent = values.mode === 'silent'\n const body = (values.body ?? '').trim()\n\n const dataMap: Record<string, string> = {}\n for (const pair of Array.isArray(values.data) ? values.data : []) {\n const key = (pair?.key ?? '').trim()\n if (key) dataMap[key] = `${pair?.value ?? ''}`\n }\n const hasData = Object.keys(dataMap).length > 0\n\n // Options that only affect a visible banner are dropped for silent.\n const pushOptions: Record<string, unknown> = {}\n if (!silent) {\n if (typeof values.sound === 'string' && values.sound.trim()) pushOptions.sound = values.sound.trim()\n const badgeNum = typeof values.badge === 'number' ? values.badge : Number.parseInt(String(values.badge ?? ''), 10)\n if (Number.isFinite(badgeNum) && badgeNum >= 0) pushOptions.badge = badgeNum\n if (typeof values.image === 'string' && values.image.trim()) pushOptions.image = values.image.trim()\n if (typeof values.channelId === 'string' && values.channelId.trim()) pushOptions.channelId = values.channelId.trim()\n }\n if (values.priority === 'high' || values.priority === 'normal') pushOptions.priority = values.priority\n const hasOptions = Object.keys(pushOptions).length > 0\n\n // Silent hides the title field; the API still needs one, so use a stable label for the log.\n const title = silent ? (values.title ?? '').trim() || 'Silent push' : (values.title ?? '').trim()\n\n await apiCallOrThrow('/api/push_notifications/custom-send', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n recipientUserId: (values.userId ?? '').trim(),\n deviceId: (values.deviceId ?? '').trim() || undefined,\n title,\n body: !silent && body.length > 0 ? body : undefined,\n silent,\n data: hasData ? dataMap : undefined,\n pushOptions: hasOptions ? pushOptions : undefined,\n }),\n })\n flash(t('push_notifications.send.success'), 'success')\n router.push(DELIVERIES_HREF)\n } finally {\n setSubmitting(false)\n }\n }}\n />\n <div className=\"mt-4 flex items-center justify-between gap-3\">\n <div className=\"text-xs\">\n {!deviceState.userId ? (\n <span className=\"text-muted-foreground\">{t('push_notifications.send.deviceSelectUserFirst')}</span>\n ) : deviceState.count === 0 && !deviceState.loading ? (\n <span className=\"text-status-warning-text\">{t('push_notifications.send.deviceNone')}</span>\n ) : null}\n </div>\n <div className=\"flex gap-2\">\n <Button asChild variant=\"outline\">\n <Link href={DELIVERIES_HREF}>{t('common.cancel', 'Cancel')}</Link>\n </Button>\n <Button\n type=\"submit\"\n form={FORM_ID}\n disabled={!deviceState.userId || deviceState.count === 0 || deviceState.loading || submitting}\n >\n {t('push_notifications.send.submit')}\n </Button>\n </div>\n </div>\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -135,6 +135,7 @@ const dateOnlySchema = z.string().trim().regex(/^\d{4}-\d{2}-\d{2}$/, { message:
135
135
  message: "invalid_date"
136
136
  });
137
137
  const addressSnapshotSchema = z.record(z.string(), z.unknown()).nullable().optional();
138
+ const exchangeRateSchema = z.coerce.number().min(0);
138
139
  const documentUpdateSchema = z.object({
139
140
  id: z.string().uuid(),
140
141
  customerEntityId: z.string().uuid().nullable().optional(),
@@ -152,6 +153,13 @@ const documentUpdateSchema = z.object({
152
153
  orderNumber: z.string().trim().min(1).max(191).optional(),
153
154
  quoteNumber: z.string().trim().min(1).max(191).optional(),
154
155
  currencyCode: currencyCodeSchema.optional(),
156
+ // Order-only columns — SalesQuote declares none of them. Declared here
157
+ // because the create schema declares them and a caller reusing its create
158
+ // payload on an update otherwise lost them silently; `applyDocumentUpdate`
159
+ // rejects each with a 400 on a quote rather than dropping it.
160
+ exchangeRate: z.union([z.null(), exchangeRateSchema]).optional(),
161
+ paymentStatusEntryId: z.string().uuid().nullable().optional(),
162
+ fulfillmentStatusEntryId: z.string().uuid().nullable().optional(),
155
163
  channelId: z.string().uuid().nullable().optional(),
156
164
  statusEntryId: z.string().uuid().nullable().optional(),
157
165
  placedAt: z.union([dateOnlySchema, z.null()]).optional(),
@@ -170,7 +178,7 @@ const documentUpdateSchema = z.object({
170
178
  customFields: z.record(z.string(), z.unknown()).optional(),
171
179
  customFieldSetId: z.string().uuid().nullable().optional()
172
180
  }).refine(
173
- (input) => typeof input.currencyCode === "string" || input.placedAt !== void 0 || input.expectedDeliveryAt !== void 0 || input.channelId !== void 0 || input.statusEntryId !== void 0 || input.shippingAddressId !== void 0 || input.billingAddressId !== void 0 || input.customerEntityId !== void 0 || input.customerContactId !== void 0 || input.customerSnapshot !== void 0 || input.metadata !== void 0 || input.customerReference !== void 0 || input.externalReference !== void 0 || input.comment !== void 0 || input.comments !== void 0 || input.internalNotes !== void 0 || input.orderNumber !== void 0 || input.quoteNumber !== void 0 || input.shippingAddressSnapshot !== void 0 || input.billingAddressSnapshot !== void 0 || input.shippingMethodId !== void 0 || input.shippingMethodCode !== void 0 || input.shippingMethodSnapshot !== void 0 || input.paymentMethodId !== void 0 || input.paymentMethodCode !== void 0 || input.paymentMethodSnapshot !== void 0 || input.tags !== void 0 || input.customFields !== void 0 || input.customFieldSetId !== void 0,
181
+ (input) => typeof input.currencyCode === "string" || input.placedAt !== void 0 || input.expectedDeliveryAt !== void 0 || input.channelId !== void 0 || input.statusEntryId !== void 0 || input.exchangeRate !== void 0 || input.paymentStatusEntryId !== void 0 || input.fulfillmentStatusEntryId !== void 0 || input.shippingAddressId !== void 0 || input.billingAddressId !== void 0 || input.customerEntityId !== void 0 || input.customerContactId !== void 0 || input.customerSnapshot !== void 0 || input.metadata !== void 0 || input.customerReference !== void 0 || input.externalReference !== void 0 || input.comment !== void 0 || input.comments !== void 0 || input.internalNotes !== void 0 || input.orderNumber !== void 0 || input.quoteNumber !== void 0 || input.shippingAddressSnapshot !== void 0 || input.billingAddressSnapshot !== void 0 || input.shippingMethodId !== void 0 || input.shippingMethodCode !== void 0 || input.shippingMethodSnapshot !== void 0 || input.paymentMethodId !== void 0 || input.paymentMethodCode !== void 0 || input.paymentMethodSnapshot !== void 0 || input.tags !== void 0 || input.customFields !== void 0 || input.customFieldSetId !== void 0,
174
182
  { message: "update_payload_empty" }
175
183
  );
176
184
  function cloneJson(value) {
@@ -542,6 +550,17 @@ async function applyDocumentUpdate({
542
550
  if (typeof input.currencyCode === "string") {
543
551
  entity.currencyCode = input.currencyCode;
544
552
  }
553
+ if (kind === "quote" && input.exchangeRate !== void 0) {
554
+ throw new CrudHttpError(400, {
555
+ error: translate(
556
+ "sales.quotes.exchange_rate_unsupported",
557
+ "Exchange rate is not available on quotes."
558
+ )
559
+ });
560
+ }
561
+ if (kind === "order" && input.exchangeRate !== void 0) {
562
+ entity.exchangeRate = toNumericString(input.exchangeRate);
563
+ }
545
564
  if (input.channelId !== void 0) {
546
565
  if (input.channelId === null) {
547
566
  entity.channelId = null;
@@ -586,6 +605,56 @@ async function applyDocumentUpdate({
586
605
  entity.statusEntryId = input.statusEntryId ?? null;
587
606
  entity.status = statusValue;
588
607
  }
608
+ if (kind === "quote" && input.paymentStatusEntryId !== void 0) {
609
+ throw new CrudHttpError(400, {
610
+ error: translate(
611
+ "sales.quotes.payment_status_unsupported",
612
+ "Payment status is not available on quotes."
613
+ )
614
+ });
615
+ }
616
+ if (kind === "order" && input.paymentStatusEntryId !== void 0) {
617
+ const paymentStatusValue = await resolveDictionaryEntryValue(
618
+ em,
619
+ input.paymentStatusEntryId,
620
+ { tenantId }
621
+ );
622
+ if (input.paymentStatusEntryId && !paymentStatusValue) {
623
+ throw new CrudHttpError(400, {
624
+ error: translate(
625
+ "sales.documents.detail.statusInvalid",
626
+ "Selected status could not be found."
627
+ )
628
+ });
629
+ }
630
+ entity.paymentStatusEntryId = input.paymentStatusEntryId ?? null;
631
+ entity.paymentStatus = paymentStatusValue;
632
+ }
633
+ if (kind === "quote" && input.fulfillmentStatusEntryId !== void 0) {
634
+ throw new CrudHttpError(400, {
635
+ error: translate(
636
+ "sales.quotes.fulfillment_status_unsupported",
637
+ "Fulfillment status is not available on quotes."
638
+ )
639
+ });
640
+ }
641
+ if (kind === "order" && input.fulfillmentStatusEntryId !== void 0) {
642
+ const fulfillmentStatusValue = await resolveDictionaryEntryValue(
643
+ em,
644
+ input.fulfillmentStatusEntryId,
645
+ { tenantId }
646
+ );
647
+ if (input.fulfillmentStatusEntryId && !fulfillmentStatusValue) {
648
+ throw new CrudHttpError(400, {
649
+ error: translate(
650
+ "sales.documents.detail.statusInvalid",
651
+ "Selected status could not be found."
652
+ )
653
+ });
654
+ }
655
+ entity.fulfillmentStatusEntryId = input.fulfillmentStatusEntryId ?? null;
656
+ entity.fulfillmentStatus = fulfillmentStatusValue;
657
+ }
589
658
  if (input.placedAt !== void 0) {
590
659
  if (input.placedAt === null) {
591
660
  entity.placedAt = null;
@@ -2464,6 +2533,15 @@ function buildDocumentUpdateChangeKeys(kind, input) {
2464
2533
  if (input.internalNotes !== void 0) keys.add("internalNotes");
2465
2534
  if (input.placedAt !== void 0) keys.add("placedAt");
2466
2535
  if (input.expectedDeliveryAt !== void 0) keys.add("expectedDeliveryAt");
2536
+ if (input.exchangeRate !== void 0) keys.add("exchangeRate");
2537
+ if (input.paymentStatusEntryId !== void 0) {
2538
+ keys.add("paymentStatusEntryId");
2539
+ keys.add("paymentStatus");
2540
+ }
2541
+ if (input.fulfillmentStatusEntryId !== void 0) {
2542
+ keys.add("fulfillmentStatusEntryId");
2543
+ keys.add("fulfillmentStatus");
2544
+ }
2467
2545
  }
2468
2546
  if (input.shippingAddressId !== void 0 || input.shippingAddressSnapshot !== void 0) {
2469
2547
  keys.add("shippingAddressId");