@open-mercato/core 0.10.1-develop.7836.1.0ef0f453c6 → 0.10.1-develop.7847.1.7d83dc5794

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 (136) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/api_keys/api/keys/route.js +15 -1
  3. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  4. package/dist/modules/auth/api/users/route.js +16 -1
  5. package/dist/modules/auth/api/users/route.js.map +2 -2
  6. package/dist/modules/auth/backend/users/page.js +13 -2
  7. package/dist/modules/auth/backend/users/page.js.map +2 -2
  8. package/dist/modules/catalog/commands/products.js +4 -2
  9. package/dist/modules/catalog/commands/products.js.map +2 -2
  10. package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
  11. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  12. package/dist/modules/customer_accounts/api/admin/users.js +24 -7
  13. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  14. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
  15. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
  16. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
  17. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  18. package/dist/modules/customer_accounts/data/validators.js +2 -1
  19. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  20. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
  21. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
  22. package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
  23. package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
  24. package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
  25. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  26. package/dist/modules/customers/cli.js +28 -110
  27. package/dist/modules/customers/cli.js.map +3 -3
  28. package/dist/modules/customers/components/DictionarySettings.js +1 -1
  29. package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
  30. package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
  31. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
  32. package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
  33. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  34. package/dist/modules/customers/components/detail/TaskForm.js +1 -16
  35. package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
  36. package/dist/modules/customers/components/detail/TasksSection.js +38 -10
  37. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  38. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
  39. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
  40. package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
  41. package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
  42. package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
  43. package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
  44. package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
  45. package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
  46. package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
  47. package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
  48. package/dist/modules/customers/translations.js +9 -0
  49. package/dist/modules/customers/translations.js.map +7 -0
  50. package/dist/modules/directory/api/organization-branding/route.js +6 -9
  51. package/dist/modules/directory/api/organization-branding/route.js.map +2 -2
  52. package/dist/modules/directory/backend/directory/branding/page.js +15 -3
  53. package/dist/modules/directory/backend/directory/branding/page.js.map +2 -2
  54. package/dist/modules/eudr/index.js +13 -0
  55. package/dist/modules/eudr/index.js.map +2 -2
  56. package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
  57. package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
  58. package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
  59. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  60. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
  61. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  62. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
  63. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  64. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
  65. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
  66. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
  67. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
  68. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
  69. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
  70. package/dist/modules/sync_excel/index.js +11 -1
  71. package/dist/modules/sync_excel/index.js.map +2 -2
  72. package/dist/modules/translations/di.js +2 -0
  73. package/dist/modules/translations/di.js.map +2 -2
  74. package/dist/modules/wms/api/listEnrichers.js +3 -1
  75. package/dist/modules/wms/api/listEnrichers.js.map +2 -2
  76. package/dist/modules/wms/index.js +1 -1
  77. package/dist/modules/wms/index.js.map +2 -2
  78. package/package.json +7 -7
  79. package/src/modules/api_keys/api/keys/route.ts +15 -1
  80. package/src/modules/api_keys/i18n/de.json +1 -0
  81. package/src/modules/api_keys/i18n/en.json +1 -0
  82. package/src/modules/api_keys/i18n/es.json +1 -0
  83. package/src/modules/api_keys/i18n/ko.json +1 -0
  84. package/src/modules/api_keys/i18n/pl.json +1 -0
  85. package/src/modules/auth/api/users/route.ts +18 -1
  86. package/src/modules/auth/backend/users/page.tsx +14 -2
  87. package/src/modules/auth/i18n/de.json +1 -0
  88. package/src/modules/auth/i18n/en.json +1 -0
  89. package/src/modules/auth/i18n/es.json +1 -0
  90. package/src/modules/auth/i18n/ko.json +1 -0
  91. package/src/modules/auth/i18n/pl.json +1 -0
  92. package/src/modules/catalog/commands/products.ts +4 -1
  93. package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
  94. package/src/modules/customer_accounts/api/admin/users.ts +26 -7
  95. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
  96. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
  97. package/src/modules/customer_accounts/data/validators.ts +1 -0
  98. package/src/modules/customer_accounts/i18n/de.json +10 -0
  99. package/src/modules/customer_accounts/i18n/en.json +10 -0
  100. package/src/modules/customer_accounts/i18n/es.json +10 -0
  101. package/src/modules/customer_accounts/i18n/ko.json +10 -0
  102. package/src/modules/customer_accounts/i18n/pl.json +10 -0
  103. package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
  104. package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
  105. package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
  106. package/src/modules/customers/cli.ts +23 -124
  107. package/src/modules/customers/components/DictionarySettings.tsx +1 -1
  108. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
  109. package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
  110. package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
  111. package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
  112. package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
  113. package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
  114. package/src/modules/customers/i18n/de.json +81 -0
  115. package/src/modules/customers/i18n/en.json +81 -0
  116. package/src/modules/customers/i18n/es.json +81 -0
  117. package/src/modules/customers/i18n/ko.json +81 -0
  118. package/src/modules/customers/i18n/pl.json +81 -0
  119. package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
  120. package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
  121. package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
  122. package/src/modules/customers/translations.ts +5 -0
  123. package/src/modules/directory/api/organization-branding/route.ts +5 -8
  124. package/src/modules/directory/backend/directory/branding/page.tsx +15 -3
  125. package/src/modules/eudr/index.ts +13 -0
  126. package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
  127. package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
  128. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
  129. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
  130. package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
  131. package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
  132. package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
  133. package/src/modules/sync_excel/index.ts +10 -0
  134. package/src/modules/translations/di.ts +2 -0
  135. package/src/modules/wms/api/listEnrichers.ts +5 -2
  136. package/src/modules/wms/index.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowLeft, FolderOpen, MessageSquare, ShieldAlert } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport const MY_WORK_HREF = '/backend/staff/time-tracking'\nexport const ACCESS_REQUEST_ENDPOINT = '/api/staff/timesheets/access-requests'\nconst ACCESS_REQUEST_CONTEXT_ID = 'staff.time_tracking.access_request'\n\nexport type AccessRequestButtonProps = {\n /**\n * Sent with the request so a Team Leader lands on the right project team\n * drawer. It is never rendered \u2014 screen 17 note 1 forbids confirming that the\n * project exists to a user who has no right to it.\n */\n timeProjectId?: string | null\n label: string\n sentLabel: string\n variant?: 'default' | 'outline'\n}\n\nexport function AccessRequestButton({\n timeProjectId,\n label,\n sentLabel,\n variant = 'outline',\n}: AccessRequestButtonProps) {\n const t = useT()\n const [submitting, setSubmitting] = React.useState(false)\n const [sent, setSent] = React.useState(false)\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: ACCESS_REQUEST_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const requestAccess = React.useCallback(async () => {\n const payload = timeProjectId ? { timeProjectId } : {}\n setSubmitting(true)\n try {\n await runMutation({\n operation: () =>\n apiCallOrThrow(ACCESS_REQUEST_ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n }),\n context: {\n formId: ACCESS_REQUEST_CONTEXT_ID,\n resourceKind: 'staff.timesheets.access_request',\n retryLastMutation,\n },\n mutationPayload: payload,\n })\n setSent(true)\n flash(\n t(\n 'staff.time_tracking.access.request.success',\n 'Your request has been sent to the Team Leaders.',\n ),\n 'success',\n )\n } catch {\n flash(\n t('staff.time_tracking.access.request.error', 'Could not send the access request.'),\n 'error',\n )\n } finally {\n setSubmitting(false)\n }\n }, [retryLastMutation, runMutation, t, timeProjectId])\n\n return (\n <Button type=\"button\" variant={variant} onClick={requestAccess} disabled={submitting || sent}>\n <MessageSquare className=\"size-4\" aria-hidden=\"true\" />\n {sent ? sentLabel : label}\n </Button>\n )\n}\n\nexport type NoProjectAccessProps = {\n timeProjectId?: string | null\n /** Where the \"Back to My work\" action returns to. */\n myWorkHref?: string\n}\n\n/**\n * Screen 17 \u2014 a project-scoped route the caller may not open. The copy names\n * neither the customer nor the project (note 1) and covers both \"never had\n * access\" and \"access was revoked\" (note 2); navigation stays untouched because\n * the caller still has other projects (note 3).\n */\nexport function NoProjectAccess({ timeProjectId, myWorkHref = MY_WORK_HREF }: NoProjectAccessProps) {\n const t = useT()\n return (\n <Page>\n <PageBody>\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<ShieldAlert className=\"size-6\" aria-hidden=\"true\" />}\n title={t('staff.time_tracking.access.denied.title', 'You do not have access to this project')}\n description={t(\n 'staff.time_tracking.access.denied.description',\n 'Project access is granted one project at a time by a Team Leader. If you had access before, it may have been revoked \u2014 your earlier time entries stay saved.',\n )}\n actions={\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <Button type=\"button\" variant=\"default\" asChild>\n <a href={myWorkHref}>\n <ArrowLeft className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.access.denied.back', 'Back to My work')}\n </a>\n </Button>\n <AccessRequestButton\n timeProjectId={timeProjectId}\n label={t('staff.time_tracking.access.denied.request', 'Request access')}\n sentLabel={t('staff.time_tracking.access.request.sent', 'Request sent')}\n />\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n\n/**\n * Screen 2 \u2014 the caller has no project assignments at all. Rendered inside the\n * host page (which keeps its own header), so it stays a bare empty state.\n */\nexport function NoTimeProjectAssignments() {\n const t = useT()\n return (\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<FolderOpen className=\"size-6\" aria-hidden=\"true\" />}\n title={t('staff.time_tracking.access.empty.title', 'You have no projects assigned yet')}\n description={t(\n 'staff.time_tracking.access.empty.description',\n 'A Team Leader grants project access. Once you are added to a project it shows up here right away \u2014 no need to log in again.',\n )}\n actions={\n <AccessRequestButton\n variant=\"default\"\n label={t('staff.time_tracking.access.empty.request', 'Ask a Team Leader for access')}\n sentLabel={t('staff.time_tracking.access.request.sent', 'Request sent')}\n />\n }\n />\n )\n}\n\nexport default NoProjectAccess\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ArrowLeft, FolderOpen, MessageSquare, ShieldAlert } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { EmptyState } from '@open-mercato/ui/primitives/empty-state'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useProjectAccessScope } from './projectAccessScope'\nimport type { ResolvedProjectAccessScope } from './projectAccessScopeHeaders'\n\nexport const MY_WORK_HREF = '/backend/staff/time-tracking'\nexport const ACCESS_REQUEST_ENDPOINT = '/api/staff/timesheets/access-requests'\nconst ACCESS_REQUEST_CONTEXT_ID = 'staff.time_tracking.access_request'\n\nexport type AccessRequestButtonProps = {\n /**\n * Sent with the request so a Team Leader lands on the right project team\n * drawer. It is never rendered \u2014 screen 17 note 1 forbids confirming that the\n * project exists to a user who has no right to it.\n */\n timeProjectId?: string | null\n resolvedScope?: ResolvedProjectAccessScope | null\n label: string\n sentLabel: string\n variant?: 'default' | 'outline'\n}\n\nexport function AccessRequestButton(props: AccessRequestButtonProps) {\n const { scopeKey, isCurrentScope } = useProjectAccessScope(props.timeProjectId, props.resolvedScope)\n return <ScopedAccessRequestButton key={scopeKey} {...props} isCurrentScope={isCurrentScope} />\n}\n\nfunction ScopedAccessRequestButton({\n timeProjectId,\n label,\n sentLabel,\n variant = 'outline',\n isCurrentScope,\n}: AccessRequestButtonProps & { isCurrentScope: () => boolean }) {\n const t = useT()\n const [submitting, setSubmitting] = React.useState(false)\n const [sent, setSent] = React.useState(false)\n const mounted = React.useRef(true)\n React.useEffect(() => {\n mounted.current = true\n return () => { mounted.current = false }\n }, [])\n const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({\n contextId: ACCESS_REQUEST_CONTEXT_ID,\n blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),\n })\n\n const requestAccess = React.useCallback(async () => {\n const isCurrent = () => mounted.current && isCurrentScope()\n if (!isCurrent()) return\n const payload = timeProjectId ? { timeProjectId } : {}\n setSubmitting(true)\n try {\n await runMutation({\n operation: () => {\n if (!isCurrent()) throw new Error('[internal] Access request scope changed')\n return apiCallOrThrow(ACCESS_REQUEST_ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n },\n context: {\n formId: ACCESS_REQUEST_CONTEXT_ID,\n resourceKind: 'staff.timesheets.access_request',\n retryLastMutation,\n },\n mutationPayload: payload,\n })\n if (!isCurrent()) return\n setSent(true)\n flash(\n t(\n 'staff.time_tracking.access.request.success',\n 'Your request has been sent to the Team Leaders.',\n ),\n 'success',\n )\n } catch {\n if (!isCurrent()) return\n flash(\n t('staff.time_tracking.access.request.error', 'Could not send the access request.'),\n 'error',\n )\n } finally {\n if (isCurrent()) setSubmitting(false)\n }\n }, [isCurrentScope, retryLastMutation, runMutation, t, timeProjectId])\n\n return (\n <Button type=\"button\" variant={variant} onClick={requestAccess} disabled={submitting || sent}>\n <MessageSquare className=\"size-4\" aria-hidden=\"true\" />\n {sent ? sentLabel : label}\n </Button>\n )\n}\n\nexport type NoProjectAccessProps = {\n timeProjectId?: string | null\n resolvedScope?: ResolvedProjectAccessScope | null\n /** Where the \"Back to My work\" action returns to. */\n myWorkHref?: string\n}\n\n/**\n * Screen 17 \u2014 a project-scoped route the caller may not open. The copy names\n * neither the customer nor the project (note 1) and covers both \"never had\n * access\" and \"access was revoked\" (note 2); navigation stays untouched because\n * the caller still has other projects (note 3).\n */\nexport function NoProjectAccess({ timeProjectId, resolvedScope, myWorkHref = MY_WORK_HREF }: NoProjectAccessProps) {\n const t = useT()\n return (\n <Page>\n <PageBody>\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<ShieldAlert className=\"size-6\" aria-hidden=\"true\" />}\n title={t('staff.time_tracking.access.denied.title', 'You do not have access to this project')}\n description={t(\n 'staff.time_tracking.access.denied.description',\n 'Project access is granted one project at a time by a Team Leader. If you had access before, it may have been revoked \u2014 your earlier time entries stay saved.',\n )}\n actions={\n <div className=\"flex flex-wrap items-center justify-center gap-2\">\n <Button type=\"button\" variant=\"default\" asChild>\n <a href={myWorkHref}>\n <ArrowLeft className=\"size-4\" aria-hidden=\"true\" />\n {t('staff.time_tracking.access.denied.back', 'Back to My work')}\n </a>\n </Button>\n <AccessRequestButton\n timeProjectId={timeProjectId}\n resolvedScope={resolvedScope}\n label={t('staff.time_tracking.access.denied.request', 'Request access')}\n sentLabel={t('staff.time_tracking.access.request.sent', 'Request sent')}\n />\n </div>\n }\n />\n </PageBody>\n </Page>\n )\n}\n\n/**\n * Screen 2 \u2014 the caller has no project assignments at all. Rendered inside the\n * host page (which keeps its own header), so it stays a bare empty state.\n */\nexport function NoTimeProjectAssignments() {\n const t = useT()\n return (\n <EmptyState\n variant=\"subtle\"\n size=\"lg\"\n icon={<FolderOpen className=\"size-6\" aria-hidden=\"true\" />}\n title={t('staff.time_tracking.access.empty.title', 'You have no projects assigned yet')}\n description={t(\n 'staff.time_tracking.access.empty.description',\n 'A Team Leader grants project access. Once you are added to a project it shows up here right away \u2014 no need to log in again.',\n )}\n actions={\n <AccessRequestButton\n variant=\"default\"\n label={t('staff.time_tracking.access.empty.request', 'Ask a Team Leader for access')}\n sentLabel={t('staff.time_tracking.access.request.sent', 'Request sent')}\n />\n }\n />\n )\n}\n\nexport default NoProjectAccess\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { useOrganizationScopeVersion } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
4
+ import {
5
+ parseSelectedOrganizationCookie,
6
+ parseSelectedTenantCookie
7
+ } from "@open-mercato/core/modules/directory/utils/scopeCookies";
8
+ function readProjectAccessScope(timeProjectId) {
9
+ const cookieHeader = typeof document === "undefined" ? null : document.cookie;
10
+ return {
11
+ projectId: timeProjectId ?? null,
12
+ tenantId: parseSelectedTenantCookie(cookieHeader),
13
+ organizationId: parseSelectedOrganizationCookie(cookieHeader)
14
+ };
15
+ }
16
+ function bindProjectAccessScope(bound, current, resolvedScope) {
17
+ const continues = (boundValue, currentValue, resolvedValue) => boundValue === currentValue || boundValue === null && currentValue === resolvedValue;
18
+ if (bound.projectId !== current.projectId) return null;
19
+ if (!continues(bound.tenantId, current.tenantId, resolvedScope?.tenantId)) return null;
20
+ if (!continues(bound.organizationId, current.organizationId, resolvedScope?.organizationId)) return null;
21
+ return current;
22
+ }
23
+ function isSameProjectAccessScope(left, right) {
24
+ return left.projectId === right.projectId && left.tenantId === right.tenantId && left.organizationId === right.organizationId;
25
+ }
26
+ function rebindProjectAccessScope(binding, current) {
27
+ const continued = bindProjectAccessScope(binding.scope, current, binding.resolvedScope);
28
+ if (!continued) return { scope: current, generation: binding.generation + 1, resolvedScope: null };
29
+ if (isSameProjectAccessScope(continued, binding.scope)) return binding;
30
+ return { ...binding, scope: continued };
31
+ }
32
+ function useProjectAccessScope(timeProjectId, initialResolvedScope) {
33
+ useOrganizationScopeVersion();
34
+ const current = readProjectAccessScope(timeProjectId);
35
+ const [binding, setBinding] = React.useState(() => ({
36
+ scope: current,
37
+ generation: 0,
38
+ resolvedScope: initialResolvedScope ?? null
39
+ }));
40
+ const nextBinding = rebindProjectAccessScope(binding, current);
41
+ if (nextBinding !== binding) setBinding(nextBinding);
42
+ const boundScope = React.useRef(nextBinding);
43
+ boundScope.current = nextBinding;
44
+ const generation = nextBinding.generation;
45
+ const isCurrentScope = React.useCallback(() => {
46
+ const active = boundScope.current;
47
+ return active.generation === generation && bindProjectAccessScope(active.scope, readProjectAccessScope(timeProjectId), active.resolvedScope) !== null;
48
+ }, [generation, timeProjectId]);
49
+ const resolveScope = React.useCallback((resolvedScope) => {
50
+ setBinding((previous) => {
51
+ if (previous.generation !== generation) return previous;
52
+ if (!bindProjectAccessScope(previous.scope, readProjectAccessScope(timeProjectId), resolvedScope)) return previous;
53
+ const changed = previous.resolvedScope !== null && resolvedScope !== null && (previous.resolvedScope.tenantId !== resolvedScope.tenantId || previous.resolvedScope.organizationId !== resolvedScope.organizationId);
54
+ return { ...previous, resolvedScope, generation: previous.generation + (changed ? 1 : 0) };
55
+ });
56
+ }, [generation, timeProjectId]);
57
+ return { scopeKey: String(generation), isCurrentScope, resolvedScope: nextBinding.resolvedScope, resolveScope };
58
+ }
59
+ export {
60
+ bindProjectAccessScope,
61
+ readProjectAccessScope,
62
+ useProjectAccessScope
63
+ };
64
+ //# sourceMappingURL=projectAccessScope.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts"],
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport {\n parseSelectedOrganizationCookie,\n parseSelectedTenantCookie,\n} from '@open-mercato/core/modules/directory/utils/scopeCookies'\nimport type { ResolvedProjectAccessScope } from './projectAccessScopeHeaders'\n\nexport type ProjectAccessScope = {\n projectId: string | null\n tenantId: string | null\n organizationId: string | null\n}\n\nexport type ProjectAccessScopeState = {\n /** Stable while the scope stays the same; changes when the project, tenant or organization changes. */\n scopeKey: string\n /** Reads the scope cookies again, so it also catches a change that has not been rendered yet. */\n isCurrentScope: () => boolean\n resolvedScope: ResolvedProjectAccessScope | null\n resolveScope: (scope: ResolvedProjectAccessScope | null) => void\n}\n\ntype ProjectAccessScopeBinding = {\n scope: ProjectAccessScope\n generation: number\n resolvedScope: ResolvedProjectAccessScope | null\n}\n\nexport function readProjectAccessScope(timeProjectId?: string | null): ProjectAccessScope {\n const cookieHeader = typeof document === 'undefined' ? null : document.cookie\n return {\n projectId: timeProjectId ?? null,\n tenantId: parseSelectedTenantCookie(cookieHeader),\n organizationId: parseSelectedOrganizationCookie(cookieHeader),\n }\n}\n\nexport function bindProjectAccessScope(\n bound: ProjectAccessScope,\n current: ProjectAccessScope,\n resolvedScope?: ResolvedProjectAccessScope | null,\n): ProjectAccessScope | null {\n const continues = (boundValue: string | null, currentValue: string | null, resolvedValue?: string) =>\n boundValue === currentValue || (boundValue === null && currentValue === resolvedValue)\n if (bound.projectId !== current.projectId) return null\n if (!continues(bound.tenantId, current.tenantId, resolvedScope?.tenantId)) return null\n if (!continues(bound.organizationId, current.organizationId, resolvedScope?.organizationId)) return null\n return current\n}\n\nfunction isSameProjectAccessScope(left: ProjectAccessScope, right: ProjectAccessScope): boolean {\n return left.projectId === right.projectId\n && left.tenantId === right.tenantId\n && left.organizationId === right.organizationId\n}\n\nfunction rebindProjectAccessScope(\n binding: ProjectAccessScopeBinding,\n current: ProjectAccessScope,\n): ProjectAccessScopeBinding {\n const continued = bindProjectAccessScope(binding.scope, current, binding.resolvedScope)\n if (!continued) return { scope: current, generation: binding.generation + 1, resolvedScope: null }\n if (isSameProjectAccessScope(continued, binding.scope)) return binding\n return { ...binding, scope: continued }\n}\n\nexport function useProjectAccessScope(\n timeProjectId?: string | null,\n initialResolvedScope?: ResolvedProjectAccessScope | null,\n): ProjectAccessScopeState {\n useOrganizationScopeVersion()\n const current = readProjectAccessScope(timeProjectId)\n const [binding, setBinding] = React.useState<ProjectAccessScopeBinding>(() => ({\n scope: current,\n generation: 0,\n resolvedScope: initialResolvedScope ?? null,\n }))\n const nextBinding = rebindProjectAccessScope(binding, current)\n if (nextBinding !== binding) setBinding(nextBinding)\n const boundScope = React.useRef(nextBinding)\n boundScope.current = nextBinding\n const generation = nextBinding.generation\n const isCurrentScope = React.useCallback(() => {\n const active = boundScope.current\n return active.generation === generation\n && bindProjectAccessScope(active.scope, readProjectAccessScope(timeProjectId), active.resolvedScope) !== null\n }, [generation, timeProjectId])\n const resolveScope = React.useCallback((resolvedScope: ResolvedProjectAccessScope | null) => {\n setBinding((previous) => {\n if (previous.generation !== generation) return previous\n if (!bindProjectAccessScope(previous.scope, readProjectAccessScope(timeProjectId), resolvedScope)) return previous\n const changed = previous.resolvedScope !== null && resolvedScope !== null\n && (previous.resolvedScope.tenantId !== resolvedScope.tenantId\n || previous.resolvedScope.organizationId !== resolvedScope.organizationId)\n return { ...previous, resolvedScope, generation: previous.generation + (changed ? 1 : 0) }\n })\n }, [generation, timeProjectId])\n return { scopeKey: String(generation), isCurrentScope, resolvedScope: nextBinding.resolvedScope, resolveScope }\n}\n"],
5
+ "mappings": ";AAEA,YAAY,WAAW;AACvB,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAwBA,SAAS,uBAAuB,eAAmD;AACxF,QAAM,eAAe,OAAO,aAAa,cAAc,OAAO,SAAS;AACvE,SAAO;AAAA,IACL,WAAW,iBAAiB;AAAA,IAC5B,UAAU,0BAA0B,YAAY;AAAA,IAChD,gBAAgB,gCAAgC,YAAY;AAAA,EAC9D;AACF;AAEO,SAAS,uBACd,OACA,SACA,eAC2B;AAC3B,QAAM,YAAY,CAAC,YAA2B,cAA6B,kBACzE,eAAe,gBAAiB,eAAe,QAAQ,iBAAiB;AAC1E,MAAI,MAAM,cAAc,QAAQ,UAAW,QAAO;AAClD,MAAI,CAAC,UAAU,MAAM,UAAU,QAAQ,UAAU,eAAe,QAAQ,EAAG,QAAO;AAClF,MAAI,CAAC,UAAU,MAAM,gBAAgB,QAAQ,gBAAgB,eAAe,cAAc,EAAG,QAAO;AACpG,SAAO;AACT;AAEA,SAAS,yBAAyB,MAA0B,OAAoC;AAC9F,SAAO,KAAK,cAAc,MAAM,aAC3B,KAAK,aAAa,MAAM,YACxB,KAAK,mBAAmB,MAAM;AACrC;AAEA,SAAS,yBACP,SACA,SAC2B;AAC3B,QAAM,YAAY,uBAAuB,QAAQ,OAAO,SAAS,QAAQ,aAAa;AACtF,MAAI,CAAC,UAAW,QAAO,EAAE,OAAO,SAAS,YAAY,QAAQ,aAAa,GAAG,eAAe,KAAK;AACjG,MAAI,yBAAyB,WAAW,QAAQ,KAAK,EAAG,QAAO;AAC/D,SAAO,EAAE,GAAG,SAAS,OAAO,UAAU;AACxC;AAEO,SAAS,sBACd,eACA,sBACyB;AACzB,8BAA4B;AAC5B,QAAM,UAAU,uBAAuB,aAAa;AACpD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAoC,OAAO;AAAA,IAC7E,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,eAAe,wBAAwB;AAAA,EACzC,EAAE;AACF,QAAM,cAAc,yBAAyB,SAAS,OAAO;AAC7D,MAAI,gBAAgB,QAAS,YAAW,WAAW;AACnD,QAAM,aAAa,MAAM,OAAO,WAAW;AAC3C,aAAW,UAAU;AACrB,QAAM,aAAa,YAAY;AAC/B,QAAM,iBAAiB,MAAM,YAAY,MAAM;AAC7C,UAAM,SAAS,WAAW;AAC1B,WAAO,OAAO,eAAe,cACxB,uBAAuB,OAAO,OAAO,uBAAuB,aAAa,GAAG,OAAO,aAAa,MAAM;AAAA,EAC7G,GAAG,CAAC,YAAY,aAAa,CAAC;AAC9B,QAAM,eAAe,MAAM,YAAY,CAAC,kBAAqD;AAC3F,eAAW,CAAC,aAAa;AACvB,UAAI,SAAS,eAAe,WAAY,QAAO;AAC/C,UAAI,CAAC,uBAAuB,SAAS,OAAO,uBAAuB,aAAa,GAAG,aAAa,EAAG,QAAO;AAC1G,YAAM,UAAU,SAAS,kBAAkB,QAAQ,kBAAkB,SAC/D,SAAS,cAAc,aAAa,cAAc,YACjD,SAAS,cAAc,mBAAmB,cAAc;AAC/D,aAAO,EAAE,GAAG,UAAU,eAAe,YAAY,SAAS,cAAc,UAAU,IAAI,GAAG;AAAA,IAC3F,CAAC;AAAA,EACH,GAAG,CAAC,YAAY,aAAa,CAAC;AAC9B,SAAO,EAAE,UAAU,OAAO,UAAU,GAAG,gBAAgB,eAAe,YAAY,eAAe,aAAa;AAChH;",
6
+ "names": []
7
+ }
@@ -0,0 +1,13 @@
1
+ const PROJECT_SCOPE_TENANT_HEADER = "x-om-project-scope-tenant-id";
2
+ const PROJECT_SCOPE_ORGANIZATION_HEADER = "x-om-project-scope-organization-id";
3
+ function readResolvedProjectAccessScope(response) {
4
+ const tenantId = response.headers?.get(PROJECT_SCOPE_TENANT_HEADER);
5
+ const organizationId = response.headers?.get(PROJECT_SCOPE_ORGANIZATION_HEADER);
6
+ return tenantId && organizationId ? { tenantId, organizationId } : null;
7
+ }
8
+ export {
9
+ PROJECT_SCOPE_ORGANIZATION_HEADER,
10
+ PROJECT_SCOPE_TENANT_HEADER,
11
+ readResolvedProjectAccessScope
12
+ };
13
+ //# sourceMappingURL=projectAccessScopeHeaders.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts"],
4
+ "sourcesContent": ["export const PROJECT_SCOPE_TENANT_HEADER = 'x-om-project-scope-tenant-id'\nexport const PROJECT_SCOPE_ORGANIZATION_HEADER = 'x-om-project-scope-organization-id'\n\nexport type ResolvedProjectAccessScope = {\n tenantId: string\n organizationId: string\n}\n\nexport function readResolvedProjectAccessScope(\n response: { headers?: Pick<Headers, 'get'> },\n): ResolvedProjectAccessScope | null {\n const tenantId = response.headers?.get(PROJECT_SCOPE_TENANT_HEADER)\n const organizationId = response.headers?.get(PROJECT_SCOPE_ORGANIZATION_HEADER)\n return tenantId && organizationId ? { tenantId, organizationId } : null\n}\n"],
5
+ "mappings": "AAAO,MAAM,8BAA8B;AACpC,MAAM,oCAAoC;AAO1C,SAAS,+BACd,UACmC;AACnC,QAAM,WAAW,SAAS,SAAS,IAAI,2BAA2B;AAClE,QAAM,iBAAiB,SAAS,SAAS,IAAI,iCAAiC;AAC9E,SAAO,YAAY,iBAAiB,EAAE,UAAU,eAAe,IAAI;AACrE;",
6
+ "names": []
7
+ }
@@ -1,7 +1,17 @@
1
1
  const metadata = {
2
2
  id: "sync_excel",
3
3
  title: "Excel / CSV Import",
4
- description: "File-upload-based CSV import foundation built on top of the data sync hub."
4
+ description: "File-upload-based CSV import foundation built on top of the data sync hub.",
5
+ /**
6
+ * `customers` is a HARD dependency: `lib/adapters/customers.ts` reads `E.customers.*` at module scope.
7
+ *
8
+ * `E` only carries a key for an ENABLED module, so with `customers` switched off that read is
9
+ * `undefined.customer_entity` and it throws while the adapter is being imported — not inside a request, where
10
+ * it could be traced. Declaring it has the generator refuse the combination before anybody starts the app.
11
+ *
12
+ * `data_sync` is the hub this is built on, named in the description and now in the manifest too.
13
+ */
14
+ requires: ["customers", "data_sync"]
5
15
  };
6
16
  var sync_excel_default = metadata;
7
17
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/sync_excel/index.ts"],
4
- "sourcesContent": ["export const metadata = {\n id: 'sync_excel',\n title: 'Excel / CSV Import',\n description: 'File-upload-based CSV import foundation built on top of the data sync hub.',\n}\n\nexport default metadata\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AACf;AAEA,IAAO,qBAAQ;",
4
+ "sourcesContent": ["export const metadata = {\n id: 'sync_excel',\n title: 'Excel / CSV Import',\n description: 'File-upload-based CSV import foundation built on top of the data sync hub.',\n /**\n * `customers` is a HARD dependency: `lib/adapters/customers.ts` reads `E.customers.*` at module scope.\n *\n * `E` only carries a key for an ENABLED module, so with `customers` switched off that read is\n * `undefined.customer_entity` and it throws while the adapter is being imported \u2014 not inside a request, where\n * it could be traced. Declaring it has the generator refuse the combination before anybody starts the app.\n *\n * `data_sync` is the hub this is built on, named in the description and now in the manifest too.\n */\n requires: ['customers', 'data_sync'],\n}\n\nexport default metadata\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUb,UAAU,CAAC,aAAa,WAAW;AACrC;AAEA,IAAO,qBAAQ;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { registerTranslatableFields } from "@open-mercato/shared/lib/localizatio
2
2
  import { registerTranslationOverlayPlugin } from "@open-mercato/shared/lib/localization/overlay-plugin";
3
3
  import { registerSupportedLocalesResolver } from "@open-mercato/shared/lib/i18n/locale-registry";
4
4
  import { translatableFields as catalogFields } from "../catalog/translations.js";
5
+ import { translatableFields as customerFields } from "../customers/translations.js";
5
6
  import { translatableFields as dictionaryFields } from "../dictionaries/translations.js";
6
7
  import { translatableFields as entitiesFields } from "../entities/translations.js";
7
8
  import { translatableFields as resourcesFields } from "../resources/translations.js";
@@ -11,6 +12,7 @@ import { resolveTenantSupportedLocales } from "./lib/supported-locales.js";
11
12
  registerSupportedLocalesResolver(resolveTenantSupportedLocales);
12
13
  function register() {
13
14
  registerTranslatableFields(catalogFields);
15
+ registerTranslatableFields(customerFields);
14
16
  registerTranslatableFields(dictionaryFields);
15
17
  registerTranslatableFields(entitiesFields);
16
18
  registerTranslatableFields(resourcesFields);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/translations/di.ts"],
4
- "sourcesContent": ["import { registerTranslatableFields } from '@open-mercato/shared/lib/localization/translatable-fields'\nimport { registerTranslationOverlayPlugin } from '@open-mercato/shared/lib/localization/overlay-plugin'\nimport { registerSupportedLocalesResolver } from '@open-mercato/shared/lib/i18n/locale-registry'\nimport { translatableFields as catalogFields } from '../catalog/translations'\nimport { translatableFields as dictionaryFields } from '../dictionaries/translations'\nimport { translatableFields as entitiesFields } from '../entities/translations'\nimport { translatableFields as resourcesFields } from '../resources/translations'\nimport { applyTranslationOverlays } from './lib/apply'\nimport { resolveLocaleFromRequest } from './lib/locale'\nimport { resolveTenantSupportedLocales } from './lib/supported-locales'\n\n// Registered at module scope, not inside `register()`: module DI registrars only\n// run when the first request container is built, and the root layout resolves the\n// served locale set without ever building one. Inside `register()` the very first\n// render of a fresh process would find an empty resolver slot and serve the\n// un-narrowed set. `di.generated.ts` imports this module statically from the app\n// bootstrap, so the slot is filled at import time instead.\nregisterSupportedLocalesResolver(resolveTenantSupportedLocales)\n\nexport function register() {\n registerTranslatableFields(catalogFields)\n registerTranslatableFields(dictionaryFields)\n registerTranslatableFields(entitiesFields)\n registerTranslatableFields(resourcesFields)\n registerTranslationOverlayPlugin(applyTranslationOverlays, resolveLocaleFromRequest)\n}\n"],
5
- "mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,wCAAwC;AACjD,SAAS,wCAAwC;AACjD,SAAS,sBAAsB,qBAAqB;AACpD,SAAS,sBAAsB,wBAAwB;AACvD,SAAS,sBAAsB,sBAAsB;AACrD,SAAS,sBAAsB,uBAAuB;AACtD,SAAS,gCAAgC;AACzC,SAAS,gCAAgC;AACzC,SAAS,qCAAqC;AAQ9C,iCAAiC,6BAA6B;AAEvD,SAAS,WAAW;AACzB,6BAA2B,aAAa;AACxC,6BAA2B,gBAAgB;AAC3C,6BAA2B,cAAc;AACzC,6BAA2B,eAAe;AAC1C,mCAAiC,0BAA0B,wBAAwB;AACrF;",
4
+ "sourcesContent": ["import { registerTranslatableFields } from '@open-mercato/shared/lib/localization/translatable-fields'\nimport { registerTranslationOverlayPlugin } from '@open-mercato/shared/lib/localization/overlay-plugin'\nimport { registerSupportedLocalesResolver } from '@open-mercato/shared/lib/i18n/locale-registry'\nimport { translatableFields as catalogFields } from '../catalog/translations'\nimport { translatableFields as customerFields } from '../customers/translations'\nimport { translatableFields as dictionaryFields } from '../dictionaries/translations'\nimport { translatableFields as entitiesFields } from '../entities/translations'\nimport { translatableFields as resourcesFields } from '../resources/translations'\nimport { applyTranslationOverlays } from './lib/apply'\nimport { resolveLocaleFromRequest } from './lib/locale'\nimport { resolveTenantSupportedLocales } from './lib/supported-locales'\n\n// Registered at module scope, not inside `register()`: module DI registrars only\n// run when the first request container is built, and the root layout resolves the\n// served locale set without ever building one. Inside `register()` the very first\n// render of a fresh process would find an empty resolver slot and serve the\n// un-narrowed set. `di.generated.ts` imports this module statically from the app\n// bootstrap, so the slot is filled at import time instead.\nregisterSupportedLocalesResolver(resolveTenantSupportedLocales)\n\nexport function register() {\n registerTranslatableFields(catalogFields)\n registerTranslatableFields(customerFields)\n registerTranslatableFields(dictionaryFields)\n registerTranslatableFields(entitiesFields)\n registerTranslatableFields(resourcesFields)\n registerTranslationOverlayPlugin(applyTranslationOverlays, resolveLocaleFromRequest)\n}\n"],
5
+ "mappings": "AAAA,SAAS,kCAAkC;AAC3C,SAAS,wCAAwC;AACjD,SAAS,wCAAwC;AACjD,SAAS,sBAAsB,qBAAqB;AACpD,SAAS,sBAAsB,sBAAsB;AACrD,SAAS,sBAAsB,wBAAwB;AACvD,SAAS,sBAAsB,sBAAsB;AACrD,SAAS,sBAAsB,uBAAuB;AACtD,SAAS,gCAAgC;AACzC,SAAS,gCAAgC;AACzC,SAAS,qCAAqC;AAQ9C,iCAAiC,6BAA6B;AAEvD,SAAS,WAAW;AACzB,6BAA2B,aAAa;AACxC,6BAA2B,cAAc;AACzC,6BAA2B,gBAAgB;AAC3C,6BAA2B,cAAc;AACzC,6BAA2B,eAAe;AAC1C,mCAAiC,0BAA0B,wBAAwB;AACrF;",
6
6
  "names": []
7
7
  }
@@ -148,11 +148,13 @@ const CUSTOMER_FIELDS = ["id", "display_name"];
148
148
  async function attachVendorLabelsToListItems(payload, ctx) {
149
149
  const items = Array.isArray(payload?.items) ? payload.items : [];
150
150
  if (items.length === 0) return;
151
+ const customerEntityId = E.customers?.customer_entity;
152
+ if (!customerEntityId) return;
151
153
  const ids = uniqueIdsFromKeys(items, ["vendor_id"]);
152
154
  if (ids.length === 0) return;
153
155
  const map = await batchLoadById(
154
156
  ctx,
155
- E.customers.customer_entity,
157
+ customerEntityId,
156
158
  ids,
157
159
  CUSTOMER_FIELDS,
158
160
  "vendor"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/wms/api/listEnrichers.ts"],
4
- "sourcesContent": ["// Lightweight afterList enrichers shared across WMS list routes.\n//\n// These run inside `makeCrudRoute({ hooks: { afterList } })` and batch-resolve\n// foreign-key labels (e.g. warehouse_id -> warehouse_name) so backend tables\n// can render the human-readable label without N+1 lookups on the client.\n//\n// We deliberately use the `queryEngine` (entity-id-based) rather than ORM\n// entity classes so this file stays decoupled from `wms/data/entities`\n// internals and from the catalog ORM \u2014 same pattern used by High #5 in WMS\n// data enrichers.\n\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\n\nconst logger = createLogger('wms')\n\ntype AnyListPayload = {\n items?: Array<Record<string, unknown>>\n}\n\ntype LookupRow = Record<string, unknown> & { id?: string | null }\n\nconst LOOKUP_BATCH_SIZE = 500\n\nfunction uniqueIdsFromKeys(\n items: Array<Record<string, unknown>>,\n keys: string[],\n): string[] {\n const set = new Set<string>()\n for (const item of items) {\n for (const key of keys) {\n const value = item?.[key]\n if (typeof value === 'string' && value.length > 0) set.add(value)\n }\n }\n return Array.from(set)\n}\n\n/**\n * Internal: batch-load `entityId` rows by primary id through the QueryEngine\n * and return them keyed by id. Splits requests into chunks of 500. Errors are\n * logged (with a `source` tag for grep-ability) and degrade to an empty map so\n * the surrounding list response still goes through.\n */\nasync function batchLoadById(\n ctx: CrudCtx,\n entityId: string,\n ids: string[],\n fields: string[],\n source: string,\n): Promise<Map<string, LookupRow>> {\n if (ids.length === 0) return new Map()\n const tenantId = ctx.auth?.tenantId\n if (!tenantId) return new Map()\n const queryEngine = ctx.container.resolve('queryEngine') as QueryEngine\n const map = new Map<string, LookupRow>()\n for (let offset = 0; offset < ids.length; offset += LOOKUP_BATCH_SIZE) {\n const slice = ids.slice(offset, offset + LOOKUP_BATCH_SIZE)\n try {\n const result = await queryEngine.query<LookupRow>(entityId, {\n tenantId,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n filters: { id: { $in: slice } },\n fields,\n page: { page: 1, pageSize: slice.length },\n })\n for (const row of result.items ?? []) {\n if (row?.id && typeof row.id === 'string') map.set(row.id, row)\n }\n } catch (err) {\n logger.warn('batch lookup failed', { source, entityId, err })\n }\n }\n return map\n}\n\nfunction decorateOnce(item: Record<string, unknown>, key: string, value: unknown): void {\n if (item[key] === undefined) item[key] = value\n}\n\n// ---------------------------------------------------------------------------\n// Warehouses\n// ---------------------------------------------------------------------------\n\ntype WarehouseRow = LookupRow & { name?: string | null; code?: string | null }\n\nconst WAREHOUSE_FIELDS = ['id', 'name', 'code']\n\n/**\n * Decorate list items with `warehouse_name` and `warehouse_code` when they\n * carry a `warehouse_id` column. Mutates `payload.items` in place.\n */\nexport async function attachWarehouseLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['warehouse_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.wms.warehouse,\n ids,\n WAREHOUSE_FIELDS,\n 'warehouse',\n )) as Map<string, WarehouseRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.warehouse_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'warehouse_name', row.name ?? null)\n decorateOnce(item, 'warehouse_code', row.code ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Locations\n// ---------------------------------------------------------------------------\n\ntype LocationLookupRow = LookupRow & { code?: string | null; type?: string | null }\n\nconst LOCATION_FIELDS = ['id', 'code', 'type']\n\n/**\n * Decorate list items with location label/type for any of these foreign-key\n * columns: `location_id`, `location_from_id`, `location_to_id`. The decorated\n * fields follow the same prefix:\n *\n * location_id -> location_code, location_type\n * location_from_id -> location_from_code, location_from_type\n * location_to_id -> location_to_code, location_to_type\n *\n * Mutates `payload.items` in place.\n */\nexport async function attachLocationLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const fkColumns = ['location_id', 'location_from_id', 'location_to_id']\n const ids = uniqueIdsFromKeys(items, fkColumns)\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.wms.warehouse_location,\n ids,\n LOCATION_FIELDS,\n 'location',\n )) as Map<string, LocationLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n for (const fk of fkColumns) {\n const id = item?.[fk]\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n const prefix = fk.replace(/_id$/, '')\n decorateOnce(item, `${prefix}_code`, row.code ?? null)\n decorateOnce(item, `${prefix}_type`, row.type ?? null)\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Catalog variants\n// ---------------------------------------------------------------------------\n\ntype VariantLookupRow = LookupRow & {\n name?: string | null\n sku?: string | null\n product_id?: string | null\n}\n\nconst VARIANT_FIELDS = ['id', 'name', 'sku', 'product_id']\n\n/**\n * Decorate list items with `variant_name`, `variant_sku`, and `product_id`\n * when they carry a `catalog_variant_id` column. Mutates `payload.items` in\n * place. Uses `queryEngine.query(E.catalog.catalog_product_variant, ...)`\n * rather than importing the catalog ORM entity directly, to keep the WMS\n * module decoupled from the catalog package internals.\n */\nexport async function attachVariantLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['catalog_variant_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.catalog.catalog_product_variant,\n ids,\n VARIANT_FIELDS,\n 'variant',\n )) as Map<string, VariantLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.catalog_variant_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'variant_name', row.name ?? null)\n decorateOnce(item, 'variant_sku', row.sku ?? null)\n decorateOnce(item, 'catalog_product_id', row.product_id ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Catalog products\n// ---------------------------------------------------------------------------\n\ntype ProductLookupRow = LookupRow & { title?: string | null; sku?: string | null }\n\nconst PRODUCT_FIELDS = ['id', 'title', 'sku']\n\n/**\n * Decorate list items with `product_title` and `product_sku` when they carry a\n * `catalog_product_id` column. Mutates `payload.items` in place.\n */\nexport async function attachProductLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['catalog_product_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.catalog.catalog_product,\n ids,\n PRODUCT_FIELDS,\n 'product',\n )) as Map<string, ProductLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.catalog_product_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'product_title', row.title ?? null)\n decorateOnce(item, 'product_sku', row.sku ?? null)\n }\n}\n\n/**\n * Decorate inventory profile rows with catalog product and variant labels.\n * Mutates `payload.items` in place.\n */\nexport async function attachInventoryProfileCatalogLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n await Promise.all([\n attachProductLabelsToListItems(payload, ctx),\n attachVariantLabelsToListItems(payload, ctx),\n ])\n}\n\n// ---------------------------------------------------------------------------\n// Customers (ASN vendors)\n// ---------------------------------------------------------------------------\n\ntype CustomerLookupRow = LookupRow & { display_name?: string | null }\n\nconst CUSTOMER_FIELDS = ['id', 'display_name']\n\n/**\n * Decorate list items with `vendor_name` when they carry a `vendor_id` column\n * referencing `customers.customer_entity` by UUID only (no ORM cross-module\n * relations). Mutates `payload.items` in place.\n */\nexport async function attachVendorLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['vendor_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.customers.customer_entity,\n ids,\n CUSTOMER_FIELDS,\n 'vendor',\n )) as Map<string, CustomerLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.vendor_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'vendor_name', row.display_name ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Reservation sources\n// ---------------------------------------------------------------------------\n\ntype SalesOrderLookupRow = LookupRow & { order_number?: string | null }\n\nconst SALES_ORDER_FIELDS = ['id', 'order_number']\n\n/**\n * Decorate reservation rows with `source_label` when the source can be resolved\n * to a human-readable reference (currently sales orders). Mutates\n * `payload.items` in place.\n */\nexport async function attachReservationSourceLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n\n const orderIds = uniqueIdsFromKeys(\n items.filter((item) => item?.source_type === 'order'),\n ['source_id'],\n )\n if (orderIds.length === 0) return\n\n const map = (await batchLoadById(\n ctx,\n E.sales.sales_order,\n orderIds,\n SALES_ORDER_FIELDS,\n 'reservation-source',\n )) as Map<string, SalesOrderLookupRow>\n if (map.size === 0) return\n\n for (const item of items) {\n if (item?.source_type !== 'order') continue\n const id = item?.source_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n const orderNumber = row?.order_number?.trim()\n if (orderNumber) decorateOnce(item, 'source_label', orderNumber)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// Lightweight afterList enrichers shared across WMS list routes.\n//\n// These run inside `makeCrudRoute({ hooks: { afterList } })` and batch-resolve\n// foreign-key labels (e.g. warehouse_id -> warehouse_name) so backend tables\n// can render the human-readable label without N+1 lookups on the client.\n//\n// We deliberately use the `queryEngine` (entity-id-based) rather than ORM\n// entity classes so this file stays decoupled from `wms/data/entities`\n// internals and from the catalog ORM \u2014 same pattern used by High #5 in WMS\n// data enrichers.\n\nimport type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport type { QueryEngine } from '@open-mercato/shared/lib/query/types'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { E } from '#generated/entities.ids.generated'\n\nconst logger = createLogger('wms')\n\ntype AnyListPayload = {\n items?: Array<Record<string, unknown>>\n}\n\ntype LookupRow = Record<string, unknown> & { id?: string | null }\n\nconst LOOKUP_BATCH_SIZE = 500\n\nfunction uniqueIdsFromKeys(\n items: Array<Record<string, unknown>>,\n keys: string[],\n): string[] {\n const set = new Set<string>()\n for (const item of items) {\n for (const key of keys) {\n const value = item?.[key]\n if (typeof value === 'string' && value.length > 0) set.add(value)\n }\n }\n return Array.from(set)\n}\n\n/**\n * Internal: batch-load `entityId` rows by primary id through the QueryEngine\n * and return them keyed by id. Splits requests into chunks of 500. Errors are\n * logged (with a `source` tag for grep-ability) and degrade to an empty map so\n * the surrounding list response still goes through.\n */\nasync function batchLoadById(\n ctx: CrudCtx,\n entityId: string,\n ids: string[],\n fields: string[],\n source: string,\n): Promise<Map<string, LookupRow>> {\n if (ids.length === 0) return new Map()\n const tenantId = ctx.auth?.tenantId\n if (!tenantId) return new Map()\n const queryEngine = ctx.container.resolve('queryEngine') as QueryEngine\n const map = new Map<string, LookupRow>()\n for (let offset = 0; offset < ids.length; offset += LOOKUP_BATCH_SIZE) {\n const slice = ids.slice(offset, offset + LOOKUP_BATCH_SIZE)\n try {\n const result = await queryEngine.query<LookupRow>(entityId, {\n tenantId,\n organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined,\n organizationIds: ctx.organizationIds ?? undefined,\n filters: { id: { $in: slice } },\n fields,\n page: { page: 1, pageSize: slice.length },\n })\n for (const row of result.items ?? []) {\n if (row?.id && typeof row.id === 'string') map.set(row.id, row)\n }\n } catch (err) {\n logger.warn('batch lookup failed', { source, entityId, err })\n }\n }\n return map\n}\n\nfunction decorateOnce(item: Record<string, unknown>, key: string, value: unknown): void {\n if (item[key] === undefined) item[key] = value\n}\n\n// ---------------------------------------------------------------------------\n// Warehouses\n// ---------------------------------------------------------------------------\n\ntype WarehouseRow = LookupRow & { name?: string | null; code?: string | null }\n\nconst WAREHOUSE_FIELDS = ['id', 'name', 'code']\n\n/**\n * Decorate list items with `warehouse_name` and `warehouse_code` when they\n * carry a `warehouse_id` column. Mutates `payload.items` in place.\n */\nexport async function attachWarehouseLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['warehouse_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.wms.warehouse,\n ids,\n WAREHOUSE_FIELDS,\n 'warehouse',\n )) as Map<string, WarehouseRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.warehouse_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'warehouse_name', row.name ?? null)\n decorateOnce(item, 'warehouse_code', row.code ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Locations\n// ---------------------------------------------------------------------------\n\ntype LocationLookupRow = LookupRow & { code?: string | null; type?: string | null }\n\nconst LOCATION_FIELDS = ['id', 'code', 'type']\n\n/**\n * Decorate list items with location label/type for any of these foreign-key\n * columns: `location_id`, `location_from_id`, `location_to_id`. The decorated\n * fields follow the same prefix:\n *\n * location_id -> location_code, location_type\n * location_from_id -> location_from_code, location_from_type\n * location_to_id -> location_to_code, location_to_type\n *\n * Mutates `payload.items` in place.\n */\nexport async function attachLocationLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const fkColumns = ['location_id', 'location_from_id', 'location_to_id']\n const ids = uniqueIdsFromKeys(items, fkColumns)\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.wms.warehouse_location,\n ids,\n LOCATION_FIELDS,\n 'location',\n )) as Map<string, LocationLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n for (const fk of fkColumns) {\n const id = item?.[fk]\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n const prefix = fk.replace(/_id$/, '')\n decorateOnce(item, `${prefix}_code`, row.code ?? null)\n decorateOnce(item, `${prefix}_type`, row.type ?? null)\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Catalog variants\n// ---------------------------------------------------------------------------\n\ntype VariantLookupRow = LookupRow & {\n name?: string | null\n sku?: string | null\n product_id?: string | null\n}\n\nconst VARIANT_FIELDS = ['id', 'name', 'sku', 'product_id']\n\n/**\n * Decorate list items with `variant_name`, `variant_sku`, and `product_id`\n * when they carry a `catalog_variant_id` column. Mutates `payload.items` in\n * place. Uses `queryEngine.query(E.catalog.catalog_product_variant, ...)`\n * rather than importing the catalog ORM entity directly, to keep the WMS\n * module decoupled from the catalog package internals.\n */\nexport async function attachVariantLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['catalog_variant_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.catalog.catalog_product_variant,\n ids,\n VARIANT_FIELDS,\n 'variant',\n )) as Map<string, VariantLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.catalog_variant_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'variant_name', row.name ?? null)\n decorateOnce(item, 'variant_sku', row.sku ?? null)\n decorateOnce(item, 'catalog_product_id', row.product_id ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Catalog products\n// ---------------------------------------------------------------------------\n\ntype ProductLookupRow = LookupRow & { title?: string | null; sku?: string | null }\n\nconst PRODUCT_FIELDS = ['id', 'title', 'sku']\n\n/**\n * Decorate list items with `product_title` and `product_sku` when they carry a\n * `catalog_product_id` column. Mutates `payload.items` in place.\n */\nexport async function attachProductLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const ids = uniqueIdsFromKeys(items, ['catalog_product_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n E.catalog.catalog_product,\n ids,\n PRODUCT_FIELDS,\n 'product',\n )) as Map<string, ProductLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.catalog_product_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'product_title', row.title ?? null)\n decorateOnce(item, 'product_sku', row.sku ?? null)\n }\n}\n\n/**\n * Decorate inventory profile rows with catalog product and variant labels.\n * Mutates `payload.items` in place.\n */\nexport async function attachInventoryProfileCatalogLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n await Promise.all([\n attachProductLabelsToListItems(payload, ctx),\n attachVariantLabelsToListItems(payload, ctx),\n ])\n}\n\n// ---------------------------------------------------------------------------\n// Customers (ASN vendors)\n// ---------------------------------------------------------------------------\n\ntype CustomerLookupRow = LookupRow & { display_name?: string | null }\n\nconst CUSTOMER_FIELDS = ['id', 'display_name']\n\n/**\n * Decorate list items with `vendor_name` when they carry a `vendor_id` column\n * referencing `customers.customer_entity` by UUID only (no ORM cross-module\n * relations). No-op when the optional `customers` module is not enabled.\n * Mutates `payload.items` in place.\n */\nexport async function attachVendorLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n const customerEntityId = E.customers?.customer_entity\n if (!customerEntityId) return\n const ids = uniqueIdsFromKeys(items, ['vendor_id'])\n if (ids.length === 0) return\n const map = (await batchLoadById(\n ctx,\n customerEntityId,\n ids,\n CUSTOMER_FIELDS,\n 'vendor',\n )) as Map<string, CustomerLookupRow>\n if (map.size === 0) return\n for (const item of items) {\n const id = item?.vendor_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n if (!row) continue\n decorateOnce(item, 'vendor_name', row.display_name ?? null)\n }\n}\n\n// ---------------------------------------------------------------------------\n// Reservation sources\n// ---------------------------------------------------------------------------\n\ntype SalesOrderLookupRow = LookupRow & { order_number?: string | null }\n\nconst SALES_ORDER_FIELDS = ['id', 'order_number']\n\n/**\n * Decorate reservation rows with `source_label` when the source can be resolved\n * to a human-readable reference (currently sales orders). Mutates\n * `payload.items` in place.\n */\nexport async function attachReservationSourceLabelsToListItems(\n payload: AnyListPayload,\n ctx: CrudCtx,\n): Promise<void> {\n const items = Array.isArray(payload?.items) ? payload.items : []\n if (items.length === 0) return\n\n const orderIds = uniqueIdsFromKeys(\n items.filter((item) => item?.source_type === 'order'),\n ['source_id'],\n )\n if (orderIds.length === 0) return\n\n const map = (await batchLoadById(\n ctx,\n E.sales.sales_order,\n orderIds,\n SALES_ORDER_FIELDS,\n 'reservation-source',\n )) as Map<string, SalesOrderLookupRow>\n if (map.size === 0) return\n\n for (const item of items) {\n if (item?.source_type !== 'order') continue\n const id = item?.source_id\n if (typeof id !== 'string' || !id) continue\n const row = map.get(id)\n const orderNumber = row?.order_number?.trim()\n if (orderNumber) decorateOnce(item, 'source_label', orderNumber)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,7 +7,7 @@ const metadata = {
7
7
  author: "Open Mercato Team",
8
8
  license: "MIT",
9
9
  ejectable: true,
10
- requires: ["catalog", "sales", "feature_toggles"]
10
+ requires: ["catalog", "customers", "sales", "feature_toggles"]
11
11
  };
12
12
  import { features } from "./acl.js";
13
13
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/wms/index.ts"],
4
- "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'wms',\n title: 'Warehouse Management System',\n version: '0.1.0',\n description: 'Warehouse topology, inventory balances, reservations, and movement ledger.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n requires: ['catalog', 'sales', 'feature_toggles'],\n}\n\nexport { features } from './acl'\n"],
5
- "mappings": "AACA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU,CAAC,WAAW,SAAS,iBAAiB;AAClD;AAEA,SAAS,gBAAgB;",
4
+ "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\nimport './commands'\n\nexport const metadata: ModuleInfo = {\n name: 'wms',\n title: 'Warehouse Management System',\n version: '0.1.0',\n description: 'Warehouse topology, inventory balances, reservations, and movement ledger.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n requires: ['catalog', 'customers', 'sales', 'feature_toggles'],\n}\n\nexport { features } from './acl'\n"],
5
+ "mappings": "AACA,OAAO;AAEA,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AAAA,EACX,UAAU,CAAC,WAAW,aAAa,SAAS,iBAAiB;AAC/D;AAEA,SAAS,gBAAgB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7836.1.0ef0f453c6",
3
+ "version": "0.10.1-develop.7847.1.7d83dc5794",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.6.6"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7836.1.0ef0f453c6",
270
- "@open-mercato/shared": "0.10.1-develop.7836.1.0ef0f453c6",
271
- "@open-mercato/ui": "0.10.1-develop.7836.1.0ef0f453c6",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7847.1.7d83dc5794",
270
+ "@open-mercato/shared": "0.10.1-develop.7847.1.7d83dc5794",
271
+ "@open-mercato/ui": "0.10.1-develop.7847.1.7d83dc5794",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7836.1.0ef0f453c6",
277
- "@open-mercato/shared": "0.10.1-develop.7836.1.0ef0f453c6",
278
- "@open-mercato/ui": "0.10.1-develop.7836.1.0ef0f453c6",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7847.1.7d83dc5794",
277
+ "@open-mercato/shared": "0.10.1-develop.7847.1.7d83dc5794",
278
+ "@open-mercato/ui": "0.10.1-develop.7847.1.7d83dc5794",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -4,7 +4,7 @@ import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
4
4
  import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
5
5
  import type { CrudCtx } from '@open-mercato/shared/lib/crud/factory'
6
6
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
7
- import { Role } from '@open-mercato/core/modules/auth/data/entities'
7
+ import { Role, User } from '@open-mercato/core/modules/auth/data/entities'
8
8
  import { Organization } from '@open-mercato/core/modules/directory/data/entities'
9
9
  import { ApiKey } from '../../data/entities'
10
10
  import { createApiKeySchema } from '../../data/validators'
@@ -13,6 +13,7 @@ import { enforceTenantSelection, resolveIsSuperAdmin } from '@open-mercato/core/
13
13
  import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
14
14
  import { assertActorCanGrantRoles } from '@open-mercato/core/modules/auth/lib/grantChecks'
15
15
  import { isOrganizationAccessAllowed } from '@open-mercato/shared/lib/auth/organizationAccess'
16
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
16
17
  import type { CreateApiKeyCommandInput } from '../../commands/keys'
17
18
 
18
19
  const listQuerySchema = z.object({
@@ -138,6 +139,19 @@ async function prepareCreateCommandInput(
138
139
  })) {
139
140
  throw json({ error: translate('api_keys.errors.organizationOutOfScope', 'Organization out of scope') }, { status: 403 })
140
141
  }
142
+ if (organizationId) {
143
+ const creator = await findOneWithDecryption(
144
+ em,
145
+ User,
146
+ { id: auth.sub, tenantId: targetTenantId, deletedAt: null },
147
+ { fields: ['organizationId'] },
148
+ { tenantId: targetTenantId, organizationId: null },
149
+ )
150
+ if (creator?.organizationId?.toLowerCase() !== organizationId.toLowerCase()) {
151
+ const message = translate('api_keys.errors.organizationMustMatchCreator')
152
+ throw json({ error: message, fieldErrors: { organizationId: message } }, { status: 400 })
153
+ }
154
+ }
141
155
 
142
156
  return {
143
157
  name: input.name,
@@ -5,6 +5,7 @@
5
5
  "api_keys.copy.warning": "Bewahre diesen Schlüssel sicher auf – er kann später nicht erneut abgerufen werden.",
6
6
  "api_keys.errors.forbidden": "Verboten",
7
7
  "api_keys.errors.notFound": "Nicht gefunden",
8
+ "api_keys.errors.organizationMustMatchCreator": "Ein organisationsgebundener API-Schlüssel muss die Organisation Ihres Kontos verwenden. Wählen Sie diese Organisation aus, um einen nutzbaren Schlüssel zu erstellen.",
8
9
  "api_keys.errors.organizationOutOfScope": "Organisation außerhalb des Geltungsbereichs",
9
10
  "api_keys.errors.roleNotFound": "Rolle {identifier} nicht gefunden",
10
11
  "api_keys.errors.roleWrongTenant": "Rolle {role} gehört zu einem anderen Mandanten",
@@ -5,6 +5,7 @@
5
5
  "api_keys.copy.warning": "Keep this key safe — it cannot be retrieved later.",
6
6
  "api_keys.errors.forbidden": "Forbidden",
7
7
  "api_keys.errors.notFound": "Not found",
8
+ "api_keys.errors.organizationMustMatchCreator": "An organization-scoped API key must use your account’s organization. Select that organization to create a usable key.",
8
9
  "api_keys.errors.organizationOutOfScope": "Organization out of scope",
9
10
  "api_keys.errors.roleNotFound": "Role {identifier} not found",
10
11
  "api_keys.errors.roleWrongTenant": "Role {role} belongs to another tenant",
@@ -5,6 +5,7 @@
5
5
  "api_keys.copy.warning": "Mantén esta clave a salvo; no se puede recuperar más tarde.",
6
6
  "api_keys.errors.forbidden": "Prohibido",
7
7
  "api_keys.errors.notFound": "No encontrado",
8
+ "api_keys.errors.organizationMustMatchCreator": "Una clave API limitada a una organización debe usar la organización de tu cuenta. Selecciona esa organización para crear una clave utilizable.",
8
9
  "api_keys.errors.organizationOutOfScope": "Organización fuera del ámbito",
9
10
  "api_keys.errors.roleNotFound": "Rol {identifier} no encontrado",
10
11
  "api_keys.errors.roleWrongTenant": "El rol {role} pertenece a otro inquilino",
@@ -5,6 +5,7 @@
5
5
  "api_keys.copy.warning": "이 키를 안전하게 보관하세요 — 나중에 다시 확인할 수 없습니다.",
6
6
  "api_keys.errors.forbidden": "접근이 거부되었습니다",
7
7
  "api_keys.errors.notFound": "찾을 수 없습니다",
8
+ "api_keys.errors.organizationMustMatchCreator": "조직 범위 API 키는 계정의 조직을 사용해야 합니다. 사용 가능한 키를 만들려면 해당 조직을 선택하세요.",
8
9
  "api_keys.errors.organizationOutOfScope": "조직이 범위를 벗어났습니다",
9
10
  "api_keys.errors.roleNotFound": "역할 {identifier}을(를) 찾을 수 없습니다",
10
11
  "api_keys.errors.roleWrongTenant": "역할 {role}은(는) 다른 테넌트에 속합니다",
@@ -5,6 +5,7 @@
5
5
  "api_keys.copy.warning": "Zachowaj ten klucz w bezpiecznym miejscu – nie można go później odzyskać.",
6
6
  "api_keys.errors.forbidden": "Brak uprawnień",
7
7
  "api_keys.errors.notFound": "Nie znaleziono",
8
+ "api_keys.errors.organizationMustMatchCreator": "Klucz API przypisany do organizacji musi używać organizacji Twojego konta. Wybierz tę organizację, aby utworzyć działający klucz.",
8
9
  "api_keys.errors.organizationOutOfScope": "Organizacja poza zakresem",
9
10
  "api_keys.errors.roleNotFound": "Nie znaleziono roli {identifier}",
10
11
  "api_keys.errors.roleWrongTenant": "Rola {role} należy do innego najemcy",
@@ -255,6 +255,23 @@ export async function GET(req: Request) {
255
255
  if (superAdminUserIds.size) {
256
256
  filters.push({ id: { $nin: Array.from(superAdminUserIds) as any } })
257
257
  }
258
+ // Browsing follows the topbar organization selection; explicit `?id=`/`?ids=` lookups stay
259
+ // tenant-wide so callers resolving a known user (author labels, assignee names) keep working.
260
+ if (userIdFilter.kind !== 'ids') {
261
+ const scope = await resolveOrganizationScopeForRequest({
262
+ container,
263
+ auth,
264
+ request: req,
265
+ tenantId: actorTenantId,
266
+ })
267
+ effectiveSelectedOrganizationId = scope.selectedId
268
+ if (Array.isArray(scope.filterIds)) {
269
+ if (scope.filterIds.length === 0) {
270
+ return NextResponse.json({ items: [], total: 0, totalPages: 1, isSuperAdmin })
271
+ }
272
+ effectiveOrganizationIds = scope.filterIds
273
+ }
274
+ }
258
275
  } else {
259
276
  const selectedTenantId = getSelectedTenantFromRequest(req)
260
277
  if (typeof selectedTenantId === 'string' && selectedTenantId.trim().length > 0) {
@@ -726,7 +743,7 @@ export const openApi: OpenApiRouteDoc = {
726
743
  GET: {
727
744
  summary: 'List users',
728
745
  description:
729
- 'Non-superadmins with auth.users.list receive a tenant-wide staff directory by default, excluding superadmin accounts. Neither the active organization nor the caller\'s organization visibility list narrows this default. Directory visibility does not grant access to organization-owned records. Super administrators may scope the response via the topbar tenant and organization selection; without a selected tenant, their list can span tenants. Search matches email, organization name, and role name. Pass organizationId for an exact organization filter, or scopeToActiveOrganization=1 to restrict results to the caller\'s exact active organization (used by recipient/assignee pickers); when the caller has no active organization, this selects only users with no organization. Both filters apply when supplied together. These are optional list filters, not authorization boundaries. Pass ids=<uuid>,<uuid> (max 100) to resolve a known set of users in one request, for example to label a list of foreign keys; it intersects with id and roleId, and a supplied ids value that contains no valid identifier matches nothing.',
746
+ 'Non-superadmins with auth.users.list browse users in the selected organization and its descendants, bounded by their tenant and organization visibility, excluding superadmin accounts. Selecting all organizations remains bounded by organization visibility. Explicit id/ids lookups retain tenant-wide staff-reference visibility, excluding superadmin accounts. Directory visibility does not grant access to organization-owned records. Super administrators may scope the response via the topbar tenant and organization selection; without a selected tenant, their list can span tenants. Search matches email, organization name, and role name. Pass organizationId for an exact organization filter, or scopeToActiveOrganization=1 to restrict results to the caller\'s exact active organization (used by recipient/assignee pickers); when the caller has no active organization, this selects only users with no organization. Both filters apply when supplied together. These optional list filters further narrow the effective scope. Pass ids=<uuid>,<uuid> (max 100) to resolve a known set of users in one request, for example to label a list of foreign keys; it intersects with id and roleId, and a supplied ids value that contains no valid identifier matches nothing.',
730
747
  query: querySchema,
731
748
  responses: [
732
749
  { status: 200, description: 'User collection', schema: userListResponseSchema },
@@ -21,6 +21,7 @@ import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/u
21
21
  import { useT } from '@open-mercato/shared/lib/i18n/context'
22
22
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
23
23
  import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
24
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
24
25
 
25
26
  type Row = {
26
27
  id: string
@@ -348,7 +349,7 @@ export default function UsersListPage() {
348
349
  return p.toString()
349
350
  }, [filterValues.name, page, search, organizationId, normalizedRoleIds])
350
351
 
351
- const { data: usersData, isLoading } = useQuery({
352
+ const { data: usersData, isLoading, isPending, isFetching, isError, errorUpdatedAt, refetch } = useQuery({
352
353
  queryKey: ['users', params, scopeVersion],
353
354
  queryFn: async () => {
354
355
  const call = await apiCall<{ items: Row[]; total: number; totalPages: number; totalIsCapped?: boolean; isSuperAdmin?: boolean }>(
@@ -361,6 +362,7 @@ export default function UsersListPage() {
361
362
  },
362
363
  })
363
364
 
365
+ const hasLoadError = isError || (isPending && errorUpdatedAt > 0)
364
366
  const rows = usersData?.items || []
365
367
  const total = usersData?.total || 0
366
368
  const totalPages = usersData?.totalPages || 1
@@ -459,7 +461,17 @@ export default function UsersListPage() {
459
461
  ]} />
460
462
  )}
461
463
  pagination={{ page, pageSize: 50, total, totalPages, totalIsCapped, onPageChange: setPage }}
462
- isLoading={isLoading}
464
+ isLoading={isLoading && !hasLoadError}
465
+ error={hasLoadError ? (
466
+ <ErrorMessage
467
+ label={t('auth.users.list.error.load', 'Failed to load users')}
468
+ action={(
469
+ <Button type="button" variant="outline" disabled={isFetching} onClick={() => { void refetch() }}>
470
+ {t('auth.users.list.actions.retry', 'Retry')}
471
+ </Button>
472
+ )}
473
+ />
474
+ ) : null}
463
475
  />
464
476
  </PageBody>
465
477
  {ConfirmDialogElement}
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Benutzerdaten werden geladen...",
711
711
  "auth.users.form.title.create": "Benutzer erstellen",
712
712
  "auth.users.form.title.edit": "Benutzer bearbeiten",
713
+ "auth.users.list.actions.retry": "Erneut versuchen",
713
714
  "auth.users.list.columns.email": "E-Mail",
714
715
  "auth.users.list.columns.name": "Anzeigename",
715
716
  "auth.users.list.columns.organization": "Organisation",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Loading user data...",
711
711
  "auth.users.form.title.create": "Create User",
712
712
  "auth.users.form.title.edit": "Edit User",
713
+ "auth.users.list.actions.retry": "Retry",
713
714
  "auth.users.list.columns.email": "Email",
714
715
  "auth.users.list.columns.name": "Display name",
715
716
  "auth.users.list.columns.organization": "Organization",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "Cargando datos del usuario...",
711
711
  "auth.users.form.title.create": "Crear usuario",
712
712
  "auth.users.form.title.edit": "Editar usuario",
713
+ "auth.users.list.actions.retry": "Reintentar",
713
714
  "auth.users.list.columns.email": "Correo electrónico",
714
715
  "auth.users.list.columns.name": "Nombre visible",
715
716
  "auth.users.list.columns.organization": "Organización",
@@ -710,6 +710,7 @@
710
710
  "auth.users.form.loading": "사용자 데이터를 불러오는 중...",
711
711
  "auth.users.form.title.create": "사용자 생성",
712
712
  "auth.users.form.title.edit": "사용자 편집",
713
+ "auth.users.list.actions.retry": "다시 시도",
713
714
  "auth.users.list.columns.email": "이메일",
714
715
  "auth.users.list.columns.name": "표시 이름",
715
716
  "auth.users.list.columns.organization": "조직",