@open-mercato/core 0.10.1-develop.7836.1.0ef0f453c6 → 0.10.1-develop.7841.1.af87ac614d

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 (102) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customer_accounts/api/admin/roles.js +25 -2
  3. package/dist/modules/customer_accounts/api/admin/roles.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/admin/users.js +24 -7
  5. package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
  6. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +77 -15
  7. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
  8. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +12 -1
  9. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  10. package/dist/modules/customer_accounts/data/validators.js +2 -1
  11. package/dist/modules/customer_accounts/data/validators.js.map +2 -2
  12. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js +46 -0
  13. package/dist/modules/customer_accounts/lib/adminOrganizationScope.js.map +7 -0
  14. package/dist/modules/customers/api/customer-dictionary-entries/route.js +86 -0
  15. package/dist/modules/customers/api/customer-dictionary-entries/route.js.map +7 -0
  16. package/dist/modules/customers/api/dictionaries/[kind]/route.js +29 -7
  17. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  18. package/dist/modules/customers/cli.js +28 -110
  19. package/dist/modules/customers/cli.js.map +3 -3
  20. package/dist/modules/customers/components/DictionarySettings.js +1 -1
  21. package/dist/modules/customers/components/DictionarySettings.js.map +1 -1
  22. package/dist/modules/customers/components/detail/ManageTagsDialog.js +1 -1
  23. package/dist/modules/customers/components/detail/ManageTagsDialog.js.map +2 -2
  24. package/dist/modules/customers/components/detail/TaskDialog.js +14 -1
  25. package/dist/modules/customers/components/detail/TaskDialog.js.map +2 -2
  26. package/dist/modules/customers/components/detail/TaskForm.js +1 -16
  27. package/dist/modules/customers/components/detail/TaskForm.js.map +2 -2
  28. package/dist/modules/customers/components/detail/TasksSection.js +38 -10
  29. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  30. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js +12 -5
  31. package/dist/modules/customers/components/detail/hooks/useCustomerDictionary.js.map +2 -2
  32. package/dist/modules/customers/components/detail/hooks/useInteractions.js +3 -13
  33. package/dist/modules/customers/components/detail/hooks/useInteractions.js.map +2 -2
  34. package/dist/modules/customers/lib/dictionaryDefaults.js +134 -0
  35. package/dist/modules/customers/lib/dictionaryDefaults.js.map +7 -0
  36. package/dist/modules/customers/lib/dictionaryLabels.js +59 -0
  37. package/dist/modules/customers/lib/dictionaryLabels.js.map +7 -0
  38. package/dist/modules/customers/lib/interactionCompatibility.js +1 -0
  39. package/dist/modules/customers/lib/interactionCompatibility.js.map +2 -2
  40. package/dist/modules/customers/translations.js +9 -0
  41. package/dist/modules/customers/translations.js.map +7 -0
  42. package/dist/modules/eudr/index.js +13 -0
  43. package/dist/modules/eudr/index.js.map +2 -2
  44. package/dist/modules/staff/api/timesheets/access-requests/route.js +11 -5
  45. package/dist/modules/staff/api/timesheets/access-requests/route.js.map +2 -2
  46. package/dist/modules/staff/api/timesheets/time-projects/route.js +19 -12
  47. package/dist/modules/staff/api/timesheets/time-projects/route.js.map +2 -2
  48. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js +21 -12
  49. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.js.map +2 -2
  50. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +14 -9
  51. package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
  52. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js +31 -10
  53. package/dist/modules/staff/lib/time-tracking-ui/NoProjectAccess.js.map +2 -2
  54. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js +64 -0
  55. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScope.js.map +7 -0
  56. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js +13 -0
  57. package/dist/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.js.map +7 -0
  58. package/dist/modules/sync_excel/index.js +11 -1
  59. package/dist/modules/sync_excel/index.js.map +2 -2
  60. package/dist/modules/translations/di.js +2 -0
  61. package/dist/modules/translations/di.js.map +2 -2
  62. package/package.json +7 -7
  63. package/src/modules/customer_accounts/api/admin/roles.ts +26 -2
  64. package/src/modules/customer_accounts/api/admin/users.ts +26 -7
  65. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +96 -13
  66. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +9 -1
  67. package/src/modules/customer_accounts/data/validators.ts +1 -0
  68. package/src/modules/customer_accounts/i18n/de.json +10 -0
  69. package/src/modules/customer_accounts/i18n/en.json +10 -0
  70. package/src/modules/customer_accounts/i18n/es.json +10 -0
  71. package/src/modules/customer_accounts/i18n/ko.json +10 -0
  72. package/src/modules/customer_accounts/i18n/pl.json +10 -0
  73. package/src/modules/customer_accounts/lib/adminOrganizationScope.ts +71 -0
  74. package/src/modules/customers/api/customer-dictionary-entries/route.ts +82 -0
  75. package/src/modules/customers/api/dictionaries/[kind]/route.ts +29 -6
  76. package/src/modules/customers/cli.ts +23 -124
  77. package/src/modules/customers/components/DictionarySettings.tsx +1 -1
  78. package/src/modules/customers/components/detail/ManageTagsDialog.tsx +1 -1
  79. package/src/modules/customers/components/detail/TaskDialog.tsx +15 -1
  80. package/src/modules/customers/components/detail/TaskForm.tsx +1 -17
  81. package/src/modules/customers/components/detail/TasksSection.tsx +44 -14
  82. package/src/modules/customers/components/detail/hooks/useCustomerDictionary.ts +14 -8
  83. package/src/modules/customers/components/detail/hooks/useInteractions.ts +3 -4
  84. package/src/modules/customers/i18n/de.json +81 -0
  85. package/src/modules/customers/i18n/en.json +81 -0
  86. package/src/modules/customers/i18n/es.json +81 -0
  87. package/src/modules/customers/i18n/ko.json +81 -0
  88. package/src/modules/customers/i18n/pl.json +81 -0
  89. package/src/modules/customers/lib/dictionaryDefaults.ts +137 -0
  90. package/src/modules/customers/lib/dictionaryLabels.ts +73 -0
  91. package/src/modules/customers/lib/interactionCompatibility.ts +1 -0
  92. package/src/modules/customers/translations.ts +5 -0
  93. package/src/modules/eudr/index.ts +13 -0
  94. package/src/modules/staff/api/timesheets/access-requests/route.ts +13 -14
  95. package/src/modules/staff/api/timesheets/time-projects/route.ts +22 -13
  96. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/edit/page.tsx +25 -14
  97. package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +14 -13
  98. package/src/modules/staff/lib/time-tracking-ui/NoProjectAccess.tsx +30 -8
  99. package/src/modules/staff/lib/time-tracking-ui/projectAccessScope.ts +102 -0
  100. package/src/modules/staff/lib/time-tracking-ui/projectAccessScopeHeaders.ts +15 -0
  101. package/src/modules/sync_excel/index.ts +10 -0
  102. package/src/modules/translations/di.ts +2 -0
@@ -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": ";AAkCS,cAkEL,YAlEK;AAhCT,YAAY,WAAW;AAEvB,SAAS,WAAW,YAAY,eAAe,mBAAmB;AAClE,SAAS,MAAM,gBAAgB;AAC/B,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AACtB,SAAS,sBAAsB;AAC/B,SAAS,0BAA0B;AACnC,SAAS,YAAY;AACrB,SAAS,6BAA6B;AAG/B,MAAM,eAAe;AACrB,MAAM,0BAA0B;AACvC,MAAM,4BAA4B;AAe3B,SAAS,oBAAoB,OAAiC;AACnE,QAAM,EAAE,UAAU,eAAe,IAAI,sBAAsB,MAAM,eAAe,MAAM,aAAa;AACnG,SAAO,oBAAC,6BAA0C,GAAG,OAAO,kBAArB,QAAqD;AAC9F;AAEA,SAAS,0BAA0B;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AACF,GAAiE;AAC/D,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAS,KAAK;AACxD,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAS,KAAK;AAC5C,QAAM,UAAU,MAAM,OAAO,IAAI;AACjC,QAAM,UAAU,MAAM;AACpB,YAAQ,UAAU;AAClB,WAAO,MAAM;AAAE,cAAQ,UAAU;AAAA,IAAM;AAAA,EACzC,GAAG,CAAC,CAAC;AACL,QAAM,EAAE,aAAa,kBAAkB,IAAI,mBAA4C;AAAA,IACrF,WAAW;AAAA,IACX,gBAAgB,EAAE,8BAA8B,4BAA4B;AAAA,EAC9E,CAAC;AAED,QAAM,gBAAgB,MAAM,YAAY,YAAY;AAClD,UAAM,YAAY,MAAM,QAAQ,WAAW,eAAe;AAC1D,QAAI,CAAC,UAAU,EAAG;AAClB,UAAM,UAAU,gBAAgB,EAAE,cAAc,IAAI,CAAC;AACrD,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,YAAY;AAAA,QAChB,WAAW,MAAM;AACf,cAAI,CAAC,UAAU,EAAG,OAAM,IAAI,MAAM,yCAAyC;AAC3E,iBAAO,eAAe,yBAAyB;AAAA,YAC7C,QAAQ;AAAA,YACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,YAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,UAC9B,CAAC;AAAA,QACH;AAAA,QACA,SAAS;AAAA,UACP,QAAQ;AAAA,UACR,cAAc;AAAA,UACd;AAAA,QACF;AAAA,QACA,iBAAiB;AAAA,MACnB,CAAC;AACD,UAAI,CAAC,UAAU,EAAG;AAClB,cAAQ,IAAI;AACZ;AAAA,QACE;AAAA,UACE;AAAA,UACA;AAAA,QACF;AAAA,QACA;AAAA,MACF;AAAA,IACF,QAAQ;AACN,UAAI,CAAC,UAAU,EAAG;AAClB;AAAA,QACE,EAAE,4CAA4C,oCAAoC;AAAA,QAClF;AAAA,MACF;AAAA,IACF,UAAE;AACA,UAAI,UAAU,EAAG,eAAc,KAAK;AAAA,IACtC;AAAA,EACF,GAAG,CAAC,gBAAgB,mBAAmB,aAAa,GAAG,aAAa,CAAC;AAErE,SACE,qBAAC,UAAO,MAAK,UAAS,SAAkB,SAAS,eAAe,UAAU,cAAc,MACtF;AAAA,wBAAC,iBAAc,WAAU,UAAS,eAAY,QAAO;AAAA,IACpD,OAAO,YAAY;AAAA,KACtB;AAEJ;AAeO,SAAS,gBAAgB,EAAE,eAAe,eAAe,aAAa,aAAa,GAAyB;AACjH,QAAM,IAAI,KAAK;AACf,SACE,oBAAC,QACC,8BAAC,YACC;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,oBAAC,eAAY,WAAU,UAAS,eAAY,QAAO;AAAA,MACzD,OAAO,EAAE,2CAA2C,wCAAwC;AAAA,MAC5F,aAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AAAA,MACA,SACE,qBAAC,SAAI,WAAU,oDACb;AAAA,4BAAC,UAAO,MAAK,UAAS,SAAQ,WAAU,SAAO,MAC7C,+BAAC,OAAE,MAAM,YACP;AAAA,8BAAC,aAAU,WAAU,UAAS,eAAY,QAAO;AAAA,UAChD,EAAE,0CAA0C,iBAAiB;AAAA,WAChE,GACF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC;AAAA,YACA;AAAA,YACA,OAAO,EAAE,6CAA6C,gBAAgB;AAAA,YACtE,WAAW,EAAE,2CAA2C,cAAc;AAAA;AAAA,QACxE;AAAA,SACF;AAAA;AAAA,EAEJ,GACF,GACF;AAEJ;AAMO,SAAS,2BAA2B;AACzC,QAAM,IAAI,KAAK;AACf,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,oBAAC,cAAW,WAAU,UAAS,eAAY,QAAO;AAAA,MACxD,OAAO,EAAE,0CAA0C,mCAAmC;AAAA,MACtF,aAAa;AAAA,QACX;AAAA,QACA;AAAA,MACF;AAAA,MACA,SACE;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,OAAO,EAAE,4CAA4C,8BAA8B;AAAA,UACnF,WAAW,EAAE,2CAA2C,cAAc;AAAA;AAAA,MACxE;AAAA;AAAA,EAEJ;AAEJ;AAEA,IAAO,0BAAQ;",
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
  }
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.7841.1.af87ac614d",
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.7841.1.af87ac614d",
270
+ "@open-mercato/shared": "0.10.1-develop.7841.1.af87ac614d",
271
+ "@open-mercato/ui": "0.10.1-develop.7841.1.af87ac614d",
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.7841.1.af87ac614d",
277
+ "@open-mercato/shared": "0.10.1-develop.7841.1.af87ac614d",
278
+ "@open-mercato/ui": "0.10.1-develop.7841.1.af87ac614d",
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",
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
2
3
  import { z } from 'zod'
3
4
  import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
4
5
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
@@ -8,6 +9,7 @@ import { RbacService } from '@open-mercato/core/modules/auth/services/rbacServic
8
9
  import { CustomerRole, CustomerRoleAcl } from '@open-mercato/core/modules/customer_accounts/data/entities'
9
10
  import { createRoleSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'
10
11
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
12
+ import { resolveAdminTargetOrganization } from '@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope'
11
13
 
12
14
  export const metadata = {}
13
15
 
@@ -25,6 +27,21 @@ export async function GET(req: Request) {
25
27
  }
26
28
 
27
29
  const url = new URL(req.url)
30
+ const requestedOrganizationId = url.searchParams.get('organizationId')
31
+ let organizationId = auth.orgId
32
+ if (requestedOrganizationId) {
33
+ const target = await resolveAdminTargetOrganization({ container, auth, request: req, requestedOrganizationId })
34
+ if (!target.ok) return target.response
35
+ organizationId = target.organizationId
36
+ if (organizationId !== auth.orgId) {
37
+ const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.view'], { tenantId: auth.tenantId, organizationId })
38
+ if (!hasTargetAccess) {
39
+ const { translate } = await resolveTranslations()
40
+ return NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.insufficientPermissions', 'Insufficient permissions') }, { status: 403 })
41
+ }
42
+ }
43
+ }
44
+
28
45
  const page = Math.max(1, parseInt(url.searchParams.get('page') || '1', 10) || 1)
29
46
  const pageSize = Math.min(100, Math.max(1, parseInt(url.searchParams.get('pageSize') || '50', 10) || 50))
30
47
  const search = (url.searchParams.get('search') || '').trim()
@@ -33,7 +50,7 @@ export async function GET(req: Request) {
33
50
 
34
51
  const where: Record<string, unknown> = {
35
52
  tenantId: auth.tenantId,
36
- organizationId: auth.orgId,
53
+ organizationId,
37
54
  deletedAt: null,
38
55
  }
39
56
 
@@ -179,14 +196,21 @@ const errorSchema = z.object({ ok: z.literal(false), error: z.string() })
179
196
 
180
197
  const getMethodDoc: OpenApiMethodDoc = {
181
198
  summary: 'List customer roles (admin)',
182
- description: 'Returns all customer roles for the tenant.',
199
+ description: 'Returns customer roles for the caller\'s active organization, or for the organization passed as organizationId when it is within the caller\'s organization scope.',
183
200
  tags: ['Customer Accounts Admin'],
201
+ query: z.object({
202
+ page: z.number().int().positive().optional(),
203
+ pageSize: z.number().int().positive().max(100).optional(),
204
+ search: z.string().optional(),
205
+ organizationId: z.string().uuid().optional(),
206
+ }),
184
207
  responses: [{
185
208
  status: 200,
186
209
  description: 'Role list',
187
210
  schema: z.object({ ok: z.literal(true), items: z.array(roleSchema), total: z.number(), totalPages: z.number(), page: z.number() }),
188
211
  }],
189
212
  errors: [
213
+ { status: 400, description: 'Organization not found or outside the caller scope', schema: errorSchema },
190
214
  { status: 401, description: 'Not authenticated', schema: errorSchema },
191
215
  { status: 403, description: 'Insufficient permissions', schema: errorSchema },
192
216
  ],
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
2
3
  import { z } from 'zod'
3
4
  import type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'
4
5
  import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
@@ -13,6 +14,7 @@ import { adminCreateUserSchema } from '@open-mercato/core/modules/customer_accou
13
14
  import { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'
14
15
  import { findAndCountWithDecryption, findWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
15
16
  import { isOwnedCompanyEntity } from '@open-mercato/core/modules/customer_accounts/lib/customerEntityOwnership'
17
+ import { resolveAdminTargetOrganization } from '@open-mercato/core/modules/customer_accounts/lib/adminOrganizationScope'
16
18
  import { lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'
17
19
  import { E } from '#generated/entities.ids.generated'
18
20
  import { resolveSearchConfig } from '@open-mercato/shared/lib/search/config'
@@ -259,6 +261,23 @@ export async function POST(req: Request) {
259
261
  return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })
260
262
  }
261
263
 
264
+ const target = await resolveAdminTargetOrganization({
265
+ container,
266
+ auth,
267
+ request: req,
268
+ requestedOrganizationId: parsed.data.organizationId,
269
+ })
270
+ if (!target.ok) return target.response
271
+ const organizationId = target.organizationId
272
+
273
+ if (organizationId !== auth.orgId) {
274
+ const hasTargetAccess = await rbacService.userHasAllFeatures(auth.sub, ['customer_accounts.manage'], { tenantId: auth.tenantId, organizationId })
275
+ if (!hasTargetAccess) {
276
+ const { translate } = await resolveTranslations()
277
+ return NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.insufficientPermissions', 'Insufficient permissions') }, { status: 403 })
278
+ }
279
+ }
280
+
262
281
  const em = container.resolve('em') as EntityManager
263
282
  const customerUserService = container.resolve('customerUserService') as CustomerUserService
264
283
 
@@ -281,11 +300,11 @@ export async function POST(req: Request) {
281
300
  {
282
301
  id: { $in: requestedRoleIds } as any,
283
302
  tenantId: auth.tenantId,
284
- organizationId: auth.orgId,
303
+ organizationId,
285
304
  deletedAt: null,
286
305
  } as any,
287
306
  undefined,
288
- { tenantId: auth.tenantId, organizationId: auth.orgId },
307
+ { tenantId: auth.tenantId, organizationId },
289
308
  )
290
309
  if (validRoles.length !== requestedRoleIds.length) {
291
310
  const foundIds = new Set(validRoles.map((role) => role.id))
@@ -301,7 +320,7 @@ export async function POST(req: Request) {
301
320
  if (parsed.data.customerEntityId) {
302
321
  const owned = await isOwnedCompanyEntity(em, parsed.data.customerEntityId, {
303
322
  tenantId: auth.tenantId,
304
- organizationId: auth.orgId,
323
+ organizationId,
305
324
  })
306
325
  if (!owned) {
307
326
  return NextResponse.json({ ok: false, error: 'Company not found' }, { status: 400 })
@@ -312,7 +331,7 @@ export async function POST(req: Request) {
312
331
  parsed.data.email,
313
332
  parsed.data.password,
314
333
  parsed.data.displayName,
315
- { tenantId: auth.tenantId!, organizationId: auth.orgId! },
334
+ { tenantId: auth.tenantId!, organizationId },
316
335
  )
317
336
  user.emailVerifiedAt = new Date()
318
337
 
@@ -341,7 +360,7 @@ export async function POST(req: Request) {
341
360
  id: user.id,
342
361
  email: user.email,
343
362
  tenantId: auth.tenantId,
344
- organizationId: auth.orgId,
363
+ organizationId,
345
364
  createdBy: auth.sub,
346
365
  }).catch(() => undefined)
347
366
 
@@ -434,12 +453,12 @@ const getMethodDoc: OpenApiMethodDoc = {
434
453
 
435
454
  const postMethodDoc: OpenApiMethodDoc = {
436
455
  summary: 'Create customer user (admin)',
437
- description: 'Creates a new customer user directly. Staff-initiated, bypasses signup flow.',
456
+ description: 'Creates a new customer user directly. Staff-initiated, bypasses signup flow. Pass organizationId to create the user in a specific organization within the caller\'s organization scope; it defaults to the caller\'s active organization. Roles and the company link are validated against the target organization.',
438
457
  tags: ['Customer Accounts Admin'],
439
458
  requestBody: { schema: adminCreateUserSchema },
440
459
  responses: [{ status: 201, description: 'User created', schema: successSchema }],
441
460
  errors: [
442
- { status: 400, description: 'Validation failed', schema: errorSchema },
461
+ { status: 400, description: 'Validation failed, organization not found or outside the caller scope, or no organization could be resolved', schema: errorSchema },
443
462
  { status: 401, description: 'Not authenticated', schema: errorSchema },
444
463
  { status: 403, description: 'Insufficient permissions', schema: errorSchema },
445
464
  { status: 409, description: 'Email already exists', schema: errorSchema },