@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
@@ -6,6 +6,7 @@ import Link from 'next/link'
6
6
  import { useRouter } from 'next/navigation'
7
7
  import { Globe, Settings } from 'lucide-react'
8
8
  import { DataTable } from '@open-mercato/ui/backend/DataTable'
9
+ import { ErrorMessage, LoadingMessage } from '@open-mercato/ui/backend/detail'
9
10
  import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
10
11
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
11
12
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -20,6 +21,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
20
21
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
21
22
  import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
22
23
  import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
24
+ import { OrganizationSelect } from '@open-mercato/core/modules/directory/components/OrganizationSelect'
23
25
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
24
26
  import { buildPortalRootUrl, buildPortalUrlPattern } from '../../../lib/portalUrl'
25
27
  import { useDemoPortalAccounts } from '../useDemoPortalAccounts'
@@ -54,38 +56,49 @@ function formatDate(value: string | null | undefined, fallback: string): string
54
56
  return date.toLocaleDateString()
55
57
  }
56
58
 
57
- async function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {
59
+ type RoleOption = { value: string; label: string; id: string }
60
+
61
+ type OrganizationRolesState =
62
+ | { organizationId: string; status: 'loading' | 'error' }
63
+ | { organizationId: string; status: 'loaded'; options: RoleOption[] }
64
+
65
+ async function fetchRoleFilterOptions(organizationId?: string | null): Promise<RoleOption[] | null> {
58
66
  try {
67
+ const params = new URLSearchParams({ pageSize: '100' })
68
+ if (organizationId) params.set('organizationId', organizationId)
59
69
  const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(
60
- '/api/customer_accounts/admin/roles?pageSize=100',
70
+ `/api/customer_accounts/admin/roles?${params.toString()}`,
61
71
  )
62
- if (!call.ok) return []
63
- const items = Array.isArray(call.result?.items) ? call.result!.items : []
72
+ if (!call.ok || !Array.isArray(call.result?.items)) return null
73
+ const items = call.result.items
64
74
  return items
65
75
  .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')
66
76
  .map((item) => ({ value: item.id, label: item.name, id: item.id }))
67
77
  } catch {
68
- return []
78
+ return null
69
79
  }
70
80
  }
71
81
 
72
82
  function CreateUserDialog({
73
83
  open,
74
84
  onOpenChange,
75
- roleOptions,
76
85
  onCreated,
77
86
  onRunMutation,
87
+ activeOrganizationId,
78
88
  }: {
79
89
  open: boolean
80
90
  onOpenChange: (next: boolean) => void
81
- roleOptions: Array<{ id: string; label: string }>
82
91
  onCreated: () => void
83
92
  onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>
93
+ activeOrganizationId: string | null
84
94
  }) {
85
95
  const t = useT()
86
96
  const [email, setEmail] = React.useState('')
87
97
  const [displayName, setDisplayName] = React.useState('')
88
98
  const [password, setPassword] = React.useState('')
99
+ const [organizationId, setOrganizationId] = React.useState<string | null>(null)
100
+ const [organizationRoles, setOrganizationRoles] = React.useState<OrganizationRolesState | null>(null)
101
+ const [roleReloadToken, setRoleReloadToken] = React.useState(0)
89
102
  const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])
90
103
  const [isSubmitting, setIsSubmitting] = React.useState(false)
91
104
 
@@ -93,6 +106,34 @@ function CreateUserDialog({
93
106
  setEmail('')
94
107
  setDisplayName('')
95
108
  setPassword('')
109
+ setOrganizationId(null)
110
+ setSelectedRoleIds([])
111
+ }, [])
112
+
113
+ const requireOrganization = !activeOrganizationId
114
+ const roleOrganizationId = organizationId ?? activeOrganizationId
115
+
116
+ React.useEffect(() => {
117
+ if (!open || !roleOrganizationId) {
118
+ setOrganizationRoles(null)
119
+ return
120
+ }
121
+ let cancelled = false
122
+ setOrganizationRoles({ organizationId: roleOrganizationId, status: 'loading' })
123
+ fetchRoleFilterOptions(roleOrganizationId).then((opts) => {
124
+ if (cancelled) return
125
+ setOrganizationRoles(opts === null
126
+ ? { organizationId: roleOrganizationId, status: 'error' }
127
+ : { organizationId: roleOrganizationId, status: 'loaded', options: opts })
128
+ })
129
+ return () => { cancelled = true }
130
+ }, [open, roleOrganizationId, roleReloadToken])
131
+
132
+ const currentOrganizationRoles = organizationRoles?.organizationId === roleOrganizationId ? organizationRoles : null
133
+ const availableRoleOptions = currentOrganizationRoles?.status === 'loaded' ? currentOrganizationRoles.options : []
134
+
135
+ const handleOrganizationChange = React.useCallback((next: string | null) => {
136
+ setOrganizationId(next ?? null)
96
137
  setSelectedRoleIds([])
97
138
  }, [])
98
139
 
@@ -102,6 +143,10 @@ function CreateUserDialog({
102
143
  flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')
103
144
  return
104
145
  }
146
+ if (requireOrganization && !organizationId) {
147
+ flash(t('customer_accounts.admin.createUser.error.organizationRequired', 'Select an organization for the new user'), 'error')
148
+ return
149
+ }
105
150
  setIsSubmitting(true)
106
151
  try {
107
152
  await onRunMutation(async () => {
@@ -115,6 +160,7 @@ function CreateUserDialog({
115
160
  displayName: displayName.trim(),
116
161
  password,
117
162
  roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,
163
+ organizationId: organizationId ?? undefined,
118
164
  }),
119
165
  },
120
166
  )
@@ -133,7 +179,7 @@ function CreateUserDialog({
133
179
  } finally {
134
180
  setIsSubmitting(false)
135
181
  }
136
- }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])
182
+ }, [displayName, email, onCreated, onOpenChange, onRunMutation, organizationId, password, requireOrganization, resetForm, selectedRoleIds, t])
137
183
 
138
184
  const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
139
185
  if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
@@ -189,11 +235,43 @@ function CreateUserDialog({
189
235
  autoComplete="new-password"
190
236
  />
191
237
  </div>
192
- {roleOptions.length > 0 && (
238
+ <div className="space-y-2">
239
+ <label className="text-sm font-medium" htmlFor="create-organization">
240
+ {t('customer_accounts.admin.createUser.fields.organization', 'Organization')}
241
+ </label>
242
+ <OrganizationSelect
243
+ id="create-organization"
244
+ value={organizationId}
245
+ onChange={handleOrganizationChange}
246
+ required={requireOrganization}
247
+ includeEmptyOption
248
+ emptyOptionLabel={requireOrganization
249
+ ? t('customer_accounts.admin.createUser.fields.organizationPlaceholder', 'Select an organization')
250
+ : t('customer_accounts.admin.createUser.fields.organizationCurrent', 'Current organization')}
251
+ className="w-full h-9 rounded border px-2 text-sm"
252
+ />
253
+ </div>
254
+ {roleOrganizationId && (
193
255
  <div className="space-y-2">
194
256
  <p className="text-sm font-medium">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>
257
+ {currentOrganizationRoles?.status === 'error' ? (
258
+ <ErrorMessage
259
+ label={t('customer_accounts.admin.createUser.error.loadRoles', 'Could not load roles for this organization')}
260
+ action={
261
+ <Button type="button" variant="outline" size="sm" onClick={() => setRoleReloadToken((token) => token + 1)}>
262
+ {t('customer_accounts.admin.createUser.actions.retryRoles', 'Retry')}
263
+ </Button>
264
+ }
265
+ />
266
+ ) : currentOrganizationRoles?.status !== 'loaded' ? (
267
+ <LoadingMessage label={t('customer_accounts.admin.createUser.loadingRoles', 'Loading roles...')} />
268
+ ) : availableRoleOptions.length === 0 ? (
269
+ <p className="text-sm text-muted-foreground">
270
+ {t('customer_accounts.admin.createUser.noRoles', 'No roles are available for this organization')}
271
+ </p>
272
+ ) : null}
195
273
  <div className="flex flex-wrap gap-2">
196
- {roleOptions.map((role) => {
274
+ {availableRoleOptions.map((role) => {
197
275
  const isSelected = selectedRoleIds.includes(role.id)
198
276
  return (
199
277
  <Button
@@ -233,9 +311,14 @@ function CreateUserDialog({
233
311
  export type PortalUsersPageClientProps = {
234
312
  portalOrigin: string
235
313
  portalOrgSlug?: string | null
314
+ activeOrganizationId?: string | null
236
315
  }
237
316
 
238
- export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: PortalUsersPageClientProps) {
317
+ export function PortalUsersPageClient({
318
+ portalOrigin,
319
+ portalOrgSlug = null,
320
+ activeOrganizationId = null,
321
+ }: PortalUsersPageClientProps) {
239
322
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
240
323
  const t = useT()
241
324
  const router = useRouter()
@@ -274,7 +357,7 @@ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: Po
274
357
  React.useEffect(() => {
275
358
  let cancelled = false
276
359
  fetchRoleFilterOptions().then((opts) => {
277
- if (!cancelled) setRoleOptions(opts)
360
+ if (!cancelled) setRoleOptions(opts ?? [])
278
361
  })
279
362
  return () => { cancelled = true }
280
363
  }, [])
@@ -613,9 +696,9 @@ export function PortalUsersPageClient({ portalOrigin, portalOrgSlug = null }: Po
613
696
  <CreateUserDialog
614
697
  open={createDialogOpen}
615
698
  onOpenChange={setCreateDialogOpen}
616
- roleOptions={roleOptions}
617
699
  onCreated={() => setReloadToken((token) => token + 1)}
618
700
  onRunMutation={runMutationWithContext}
701
+ activeOrganizationId={activeOrganizationId}
619
702
  />
620
703
  {ConfirmDialogElement}
621
704
  </>
@@ -1,4 +1,6 @@
1
1
  import { headers } from 'next/headers'
2
+ import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
3
+ import { resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'
2
4
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
3
5
  import { resolvePortalRequestOrigin } from '../../../lib/portalUrl'
4
6
  import { resolveCurrentOrgPortalSlug } from '../../../lib/portalOrgSlug'
@@ -7,10 +9,16 @@ import { PortalUsersPageClient } from './PortalUsersPageClient'
7
9
  export default async function CustomerAccountsPage() {
8
10
  const portalOrigin = resolvePortalRequestOrigin(await headers())
9
11
  const portalOrgSlug = await resolveCurrentOrgPortalSlug()
12
+ const auth = await getAuthFromCookies().catch(() => null)
13
+ const activeOrganizationId = resolveActiveOrganizationId(auth)
10
14
  return (
11
15
  <Page>
12
16
  <PageBody className="space-y-4">
13
- <PortalUsersPageClient portalOrigin={portalOrigin} portalOrgSlug={portalOrgSlug} />
17
+ <PortalUsersPageClient
18
+ portalOrigin={portalOrigin}
19
+ portalOrgSlug={portalOrgSlug}
20
+ activeOrganizationId={activeOrganizationId}
21
+ />
14
22
  </PageBody>
15
23
  </Page>
16
24
  )
@@ -96,6 +96,7 @@ export const adminCreateUserSchema = z.object({
96
96
  displayName: displayNameField,
97
97
  roleIds: z.array(z.string().uuid()).optional(),
98
98
  customerEntityId: z.string().uuid().optional(),
99
+ organizationId: z.string().uuid().optional(),
99
100
  })
100
101
 
101
102
  export const adminUpdateUserSchema = z.object({
@@ -18,16 +18,24 @@
18
18
  "customer_accounts.admin.createUser.actions.cancel": "Abbrechen",
19
19
  "customer_accounts.admin.createUser.actions.create": "Benutzer erstellen",
20
20
  "customer_accounts.admin.createUser.actions.creating": "Erstellen...",
21
+ "customer_accounts.admin.createUser.actions.retryRoles": "Erneut versuchen",
22
+ "customer_accounts.admin.createUser.error.loadRoles": "Die Rollen dieser Organisation konnten nicht geladen werden",
23
+ "customer_accounts.admin.createUser.error.organizationRequired": "Wählen Sie eine Organisation für den neuen Benutzer aus",
21
24
  "customer_accounts.admin.createUser.error.required": "E-Mail, Name und Passwort sind erforderlich",
22
25
  "customer_accounts.admin.createUser.error.save": "Benutzer konnte nicht erstellt werden",
23
26
  "customer_accounts.admin.createUser.fields.displayName": "Anzeigename",
24
27
  "customer_accounts.admin.createUser.fields.displayNamePlaceholder": "Max Mustermann",
25
28
  "customer_accounts.admin.createUser.fields.email": "E-Mail",
26
29
  "customer_accounts.admin.createUser.fields.emailPlaceholder": "benutzer@beispiel.de",
30
+ "customer_accounts.admin.createUser.fields.organization": "Organisation",
31
+ "customer_accounts.admin.createUser.fields.organizationCurrent": "Aktuelle Organisation",
32
+ "customer_accounts.admin.createUser.fields.organizationPlaceholder": "Organisation auswählen",
27
33
  "customer_accounts.admin.createUser.fields.password": "Passwort",
28
34
  "customer_accounts.admin.createUser.fields.passwordPlaceholder": "Min. 8 Zeichen",
29
35
  "customer_accounts.admin.createUser.fields.roles": "Rollen",
30
36
  "customer_accounts.admin.createUser.flash.created": "Benutzer erstellt",
37
+ "customer_accounts.admin.createUser.loadingRoles": "Rollen werden geladen...",
38
+ "customer_accounts.admin.createUser.noRoles": "Für diese Organisation sind keine Rollen verfügbar",
31
39
  "customer_accounts.admin.createUser.title": "Kundenbenutzer erstellen",
32
40
  "customer_accounts.admin.detail.actions.backToList": "Back to list",
33
41
  "customer_accounts.admin.detail.actions.delete": "Delete",
@@ -96,6 +104,8 @@
96
104
  "customer_accounts.admin.error.delete": "Failed to delete user",
97
105
  "customer_accounts.admin.error.loadUsers": "Failed to load customer users",
98
106
  "customer_accounts.admin.error.toggleActive": "Failed to update user status",
107
+ "customer_accounts.admin.errors.insufficientPermissions": "Unzureichende Berechtigungen",
108
+ "customer_accounts.admin.errors.organizationNotFound": "Organisation nicht gefunden",
99
109
  "customer_accounts.admin.filters.active": "Active",
100
110
  "customer_accounts.admin.filters.inactive": "Inactive",
101
111
  "customer_accounts.admin.filters.role": "Role",
@@ -18,16 +18,24 @@
18
18
  "customer_accounts.admin.createUser.actions.cancel": "Cancel",
19
19
  "customer_accounts.admin.createUser.actions.create": "Create User",
20
20
  "customer_accounts.admin.createUser.actions.creating": "Creating...",
21
+ "customer_accounts.admin.createUser.actions.retryRoles": "Retry",
22
+ "customer_accounts.admin.createUser.error.loadRoles": "Could not load roles for this organization",
23
+ "customer_accounts.admin.createUser.error.organizationRequired": "Select an organization for the new user",
21
24
  "customer_accounts.admin.createUser.error.required": "Email, name, and password are required",
22
25
  "customer_accounts.admin.createUser.error.save": "Failed to create user",
23
26
  "customer_accounts.admin.createUser.fields.displayName": "Display Name",
24
27
  "customer_accounts.admin.createUser.fields.displayNamePlaceholder": "John Doe",
25
28
  "customer_accounts.admin.createUser.fields.email": "Email",
26
29
  "customer_accounts.admin.createUser.fields.emailPlaceholder": "user@example.com",
30
+ "customer_accounts.admin.createUser.fields.organization": "Organization",
31
+ "customer_accounts.admin.createUser.fields.organizationCurrent": "Current organization",
32
+ "customer_accounts.admin.createUser.fields.organizationPlaceholder": "Select an organization",
27
33
  "customer_accounts.admin.createUser.fields.password": "Password",
28
34
  "customer_accounts.admin.createUser.fields.passwordPlaceholder": "Min. 8 characters",
29
35
  "customer_accounts.admin.createUser.fields.roles": "Roles",
30
36
  "customer_accounts.admin.createUser.flash.created": "User created",
37
+ "customer_accounts.admin.createUser.loadingRoles": "Loading roles...",
38
+ "customer_accounts.admin.createUser.noRoles": "No roles are available for this organization",
31
39
  "customer_accounts.admin.createUser.title": "Create Customer User",
32
40
  "customer_accounts.admin.detail.actions.backToList": "Back to list",
33
41
  "customer_accounts.admin.detail.actions.delete": "Delete",
@@ -96,6 +104,8 @@
96
104
  "customer_accounts.admin.error.delete": "Failed to delete user",
97
105
  "customer_accounts.admin.error.loadUsers": "Failed to load customer users",
98
106
  "customer_accounts.admin.error.toggleActive": "Failed to update user status",
107
+ "customer_accounts.admin.errors.insufficientPermissions": "Insufficient permissions",
108
+ "customer_accounts.admin.errors.organizationNotFound": "Organization not found",
99
109
  "customer_accounts.admin.filters.active": "Active",
100
110
  "customer_accounts.admin.filters.inactive": "Inactive",
101
111
  "customer_accounts.admin.filters.role": "Role",
@@ -18,16 +18,24 @@
18
18
  "customer_accounts.admin.createUser.actions.cancel": "Cancelar",
19
19
  "customer_accounts.admin.createUser.actions.create": "Crear usuario",
20
20
  "customer_accounts.admin.createUser.actions.creating": "Creando...",
21
+ "customer_accounts.admin.createUser.actions.retryRoles": "Reintentar",
22
+ "customer_accounts.admin.createUser.error.loadRoles": "No se pudieron cargar los roles de esta organización",
23
+ "customer_accounts.admin.createUser.error.organizationRequired": "Selecciona una organización para el nuevo usuario",
21
24
  "customer_accounts.admin.createUser.error.required": "Correo, nombre y contraseña son obligatorios",
22
25
  "customer_accounts.admin.createUser.error.save": "No se pudo crear el usuario",
23
26
  "customer_accounts.admin.createUser.fields.displayName": "Nombre para mostrar",
24
27
  "customer_accounts.admin.createUser.fields.displayNamePlaceholder": "Juan García",
25
28
  "customer_accounts.admin.createUser.fields.email": "Correo electrónico",
26
29
  "customer_accounts.admin.createUser.fields.emailPlaceholder": "usuario@ejemplo.com",
30
+ "customer_accounts.admin.createUser.fields.organization": "Organización",
31
+ "customer_accounts.admin.createUser.fields.organizationCurrent": "Organización actual",
32
+ "customer_accounts.admin.createUser.fields.organizationPlaceholder": "Selecciona una organización",
27
33
  "customer_accounts.admin.createUser.fields.password": "Contraseña",
28
34
  "customer_accounts.admin.createUser.fields.passwordPlaceholder": "Mín. 8 caracteres",
29
35
  "customer_accounts.admin.createUser.fields.roles": "Roles",
30
36
  "customer_accounts.admin.createUser.flash.created": "Usuario creado",
37
+ "customer_accounts.admin.createUser.loadingRoles": "Cargando roles...",
38
+ "customer_accounts.admin.createUser.noRoles": "No hay roles disponibles para esta organización",
31
39
  "customer_accounts.admin.createUser.title": "Crear usuario de cliente",
32
40
  "customer_accounts.admin.detail.actions.backToList": "Back to list",
33
41
  "customer_accounts.admin.detail.actions.delete": "Delete",
@@ -96,6 +104,8 @@
96
104
  "customer_accounts.admin.error.delete": "Failed to delete user",
97
105
  "customer_accounts.admin.error.loadUsers": "Failed to load customer users",
98
106
  "customer_accounts.admin.error.toggleActive": "Failed to update user status",
107
+ "customer_accounts.admin.errors.insufficientPermissions": "Permisos insuficientes",
108
+ "customer_accounts.admin.errors.organizationNotFound": "Organización no encontrada",
99
109
  "customer_accounts.admin.filters.active": "Active",
100
110
  "customer_accounts.admin.filters.inactive": "Inactive",
101
111
  "customer_accounts.admin.filters.role": "Role",
@@ -18,16 +18,24 @@
18
18
  "customer_accounts.admin.createUser.actions.cancel": "취소",
19
19
  "customer_accounts.admin.createUser.actions.create": "사용자 생성",
20
20
  "customer_accounts.admin.createUser.actions.creating": "생성 중...",
21
+ "customer_accounts.admin.createUser.actions.retryRoles": "다시 시도",
22
+ "customer_accounts.admin.createUser.error.loadRoles": "이 조직의 역할을 불러오지 못했습니다",
23
+ "customer_accounts.admin.createUser.error.organizationRequired": "새 사용자의 조직을 선택하세요",
21
24
  "customer_accounts.admin.createUser.error.required": "이메일, 이름, 비밀번호는 필수입니다",
22
25
  "customer_accounts.admin.createUser.error.save": "사용자 생성에 실패했습니다",
23
26
  "customer_accounts.admin.createUser.fields.displayName": "표시 이름",
24
27
  "customer_accounts.admin.createUser.fields.displayNamePlaceholder": "홍길동",
25
28
  "customer_accounts.admin.createUser.fields.email": "이메일",
26
29
  "customer_accounts.admin.createUser.fields.emailPlaceholder": "user@example.com",
30
+ "customer_accounts.admin.createUser.fields.organization": "조직",
31
+ "customer_accounts.admin.createUser.fields.organizationCurrent": "현재 조직",
32
+ "customer_accounts.admin.createUser.fields.organizationPlaceholder": "조직 선택",
27
33
  "customer_accounts.admin.createUser.fields.password": "비밀번호",
28
34
  "customer_accounts.admin.createUser.fields.passwordPlaceholder": "최소 8자",
29
35
  "customer_accounts.admin.createUser.fields.roles": "역할",
30
36
  "customer_accounts.admin.createUser.flash.created": "사용자가 생성되었습니다",
37
+ "customer_accounts.admin.createUser.loadingRoles": "역할을 불러오는 중...",
38
+ "customer_accounts.admin.createUser.noRoles": "이 조직에 사용 가능한 역할이 없습니다",
31
39
  "customer_accounts.admin.createUser.title": "고객 사용자 생성",
32
40
  "customer_accounts.admin.detail.actions.backToList": "목록으로 돌아가기",
33
41
  "customer_accounts.admin.detail.actions.delete": "삭제",
@@ -96,6 +104,8 @@
96
104
  "customer_accounts.admin.error.delete": "사용자 삭제에 실패했습니다",
97
105
  "customer_accounts.admin.error.loadUsers": "고객 사용자를 불러오지 못했습니다",
98
106
  "customer_accounts.admin.error.toggleActive": "사용자 상태 업데이트에 실패했습니다",
107
+ "customer_accounts.admin.errors.insufficientPermissions": "권한이 부족합니다",
108
+ "customer_accounts.admin.errors.organizationNotFound": "조직을 찾을 수 없습니다",
99
109
  "customer_accounts.admin.filters.active": "활성",
100
110
  "customer_accounts.admin.filters.inactive": "비활성",
101
111
  "customer_accounts.admin.filters.role": "역할",
@@ -18,16 +18,24 @@
18
18
  "customer_accounts.admin.createUser.actions.cancel": "Anuluj",
19
19
  "customer_accounts.admin.createUser.actions.create": "Utwórz użytkownika",
20
20
  "customer_accounts.admin.createUser.actions.creating": "Tworzenie...",
21
+ "customer_accounts.admin.createUser.actions.retryRoles": "Spróbuj ponownie",
22
+ "customer_accounts.admin.createUser.error.loadRoles": "Nie udało się wczytać ról tej organizacji",
23
+ "customer_accounts.admin.createUser.error.organizationRequired": "Wybierz organizację dla nowego użytkownika",
21
24
  "customer_accounts.admin.createUser.error.required": "E-mail, nazwa i hasło są wymagane",
22
25
  "customer_accounts.admin.createUser.error.save": "Nie udało się utworzyć użytkownika",
23
26
  "customer_accounts.admin.createUser.fields.displayName": "Nazwa wyświetlana",
24
27
  "customer_accounts.admin.createUser.fields.displayNamePlaceholder": "Jan Kowalski",
25
28
  "customer_accounts.admin.createUser.fields.email": "E-mail",
26
29
  "customer_accounts.admin.createUser.fields.emailPlaceholder": "uzytkownik@przyklad.pl",
30
+ "customer_accounts.admin.createUser.fields.organization": "Organizacja",
31
+ "customer_accounts.admin.createUser.fields.organizationCurrent": "Bieżąca organizacja",
32
+ "customer_accounts.admin.createUser.fields.organizationPlaceholder": "Wybierz organizację",
27
33
  "customer_accounts.admin.createUser.fields.password": "Hasło",
28
34
  "customer_accounts.admin.createUser.fields.passwordPlaceholder": "Min. 8 znaków",
29
35
  "customer_accounts.admin.createUser.fields.roles": "Role",
30
36
  "customer_accounts.admin.createUser.flash.created": "Użytkownik utworzony",
37
+ "customer_accounts.admin.createUser.loadingRoles": "Wczytywanie ról...",
38
+ "customer_accounts.admin.createUser.noRoles": "W tej organizacji nie ma dostępnych ról",
31
39
  "customer_accounts.admin.createUser.title": "Utwórz użytkownika klienta",
32
40
  "customer_accounts.admin.detail.actions.backToList": "Powrót do listy",
33
41
  "customer_accounts.admin.detail.actions.delete": "Usuń",
@@ -96,6 +104,8 @@
96
104
  "customer_accounts.admin.error.delete": "Nie udało się usunąć użytkownika",
97
105
  "customer_accounts.admin.error.loadUsers": "Nie udało się załadować kont klientów",
98
106
  "customer_accounts.admin.error.toggleActive": "Nie udało się zmienić statusu użytkownika",
107
+ "customer_accounts.admin.errors.insufficientPermissions": "Niewystarczające uprawnienia",
108
+ "customer_accounts.admin.errors.organizationNotFound": "Nie znaleziono organizacji",
99
109
  "customer_accounts.admin.filters.active": "Aktywny",
100
110
  "customer_accounts.admin.filters.inactive": "Nieaktywny",
101
111
  "customer_accounts.admin.filters.role": "Rola",
@@ -0,0 +1,71 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { AwilixContainer } from 'awilix'
4
+ import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
6
+ import {
7
+ organizationScopeRequiredResponse,
8
+ resolveActiveOrganizationId,
9
+ } from '@open-mercato/shared/lib/auth/organizationScope'
10
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
11
+
12
+ export type AdminTargetOrganizationResult =
13
+ | { ok: true; organizationId: string }
14
+ | { ok: false; response: Response }
15
+
16
+ const organizationIdSchema = z.string().uuid()
17
+
18
+ async function organizationNotFound(): Promise<AdminTargetOrganizationResult> {
19
+ const { translate } = await resolveTranslations()
20
+ return {
21
+ ok: false,
22
+ response: NextResponse.json({ ok: false, error: translate('customer_accounts.admin.errors.organizationNotFound', 'Organization not found') }, { status: 400 }),
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Resolves the organization a staff-initiated customer-accounts write targets.
28
+ *
29
+ * Without a request the caller's active organization is used, falling back to the
30
+ * actor's own organization under an "all organizations" selection.
31
+ *
32
+ * An explicitly requested organization is honored only when the organization-scope
33
+ * resolver grants it for the caller's own tenant: it must exist, not be deleted, and
34
+ * sit inside the caller's allowed organization set.
35
+ */
36
+ export async function resolveAdminTargetOrganization({
37
+ container,
38
+ auth,
39
+ request,
40
+ requestedOrganizationId,
41
+ }: {
42
+ container: AwilixContainer
43
+ auth: NonNullable<AuthContext>
44
+ request: Request
45
+ requestedOrganizationId?: string | null
46
+ }): Promise<AdminTargetOrganizationResult> {
47
+ const requested = typeof requestedOrganizationId === 'string' && requestedOrganizationId.trim().length > 0
48
+ ? requestedOrganizationId.trim()
49
+ : null
50
+ const activeOrganizationId = resolveActiveOrganizationId(auth)
51
+
52
+ if (!requested) {
53
+ if (!activeOrganizationId) return { ok: false, response: organizationScopeRequiredResponse() }
54
+ return { ok: true, organizationId: activeOrganizationId }
55
+ }
56
+
57
+ if (!organizationIdSchema.safeParse(requested).success) return organizationNotFound()
58
+ if (requested === activeOrganizationId) return { ok: true, organizationId: requested }
59
+
60
+ const scope = await resolveOrganizationScopeForRequest({
61
+ container,
62
+ auth,
63
+ request,
64
+ selectedId: requested,
65
+ tenantId: auth.tenantId ?? null,
66
+ })
67
+ if (scope.selectionRejected || scope.selectedId !== requested || scope.tenantId !== (auth.tenantId ?? null)) {
68
+ return organizationNotFound()
69
+ }
70
+ return { ok: true, organizationId: requested }
71
+ }
@@ -0,0 +1,82 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+ import type { FilterQuery } from '@mikro-orm/core'
4
+ import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
5
+ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
6
+ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'
7
+ import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
8
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
9
+ import { CustomerDictionaryEntry } from '../../data/entities'
10
+ import { resolveDictionaryRouteContext } from '../dictionaries/context'
11
+ import { createPagedListResponseSchema } from '../openapi'
12
+
13
+ const querySchema = z.object({
14
+ page: z.coerce.number().int().min(1).default(1),
15
+ pageSize: z.coerce.number().int().min(1).max(100).default(20),
16
+ search: z.string().trim().max(150).optional(),
17
+ id: z.string().uuid().optional(),
18
+ })
19
+
20
+ export const metadata = {
21
+ GET: { requireAuth: true, requireFeatures: ['customers.settings.manage', 'translations.view'] },
22
+ }
23
+
24
+ export async function GET(req: Request) {
25
+ try {
26
+ const url = new URL(req.url)
27
+ const query = querySchema.parse({
28
+ page: url.searchParams.get('page') ?? undefined,
29
+ pageSize: url.searchParams.get('pageSize') ?? undefined,
30
+ search: url.searchParams.get('search') ?? undefined,
31
+ id: url.searchParams.get('id') ?? undefined,
32
+ })
33
+ const { em, tenantId, organizationId, translate } = await resolveDictionaryRouteContext(req)
34
+ if (!organizationId) {
35
+ return NextResponse.json({ error: translate('customers.errors.organization_required', 'Organization context is required') }, { status: 400 })
36
+ }
37
+ const where: FilterQuery<CustomerDictionaryEntry> = {
38
+ tenantId,
39
+ organizationId,
40
+ ...(query.id ? { id: query.id } : {}),
41
+ ...(query.search ? { label: { $ilike: `%${escapeLikePattern(query.search)}%` } } : {}),
42
+ }
43
+ const total = await em.count(CustomerDictionaryEntry, where)
44
+ const entries = await findWithDecryption(em, CustomerDictionaryEntry, where, {
45
+ orderBy: { kind: 'asc', label: 'asc', id: 'asc' },
46
+ limit: query.pageSize,
47
+ offset: (query.page - 1) * query.pageSize,
48
+ }, { tenantId, organizationId })
49
+ return NextResponse.json({
50
+ items: entries.map(({ id, kind, value, label, updatedAt }) => ({ id, kind, value, label, updatedAt })),
51
+ total,
52
+ page: query.page,
53
+ pageSize: query.pageSize,
54
+ totalPages: Math.ceil(total / query.pageSize),
55
+ })
56
+ } catch (error) {
57
+ if (isCrudHttpError(error)) return NextResponse.json(error.body, { status: error.status })
58
+ if (error instanceof z.ZodError) {
59
+ const { translate } = await resolveTranslations()
60
+ return NextResponse.json({ error: translate('customers.errors.invalid_query', 'Invalid query parameters') }, { status: 400 })
61
+ }
62
+ throw error
63
+ }
64
+ }
65
+
66
+ export const openApi: OpenApiRouteDoc = {
67
+ tag: 'Customers',
68
+ summary: 'Customer dictionary translation records',
69
+ methods: {
70
+ GET: {
71
+ summary: 'List base labels for translation management',
72
+ query: querySchema,
73
+ responses: [{
74
+ status: 200,
75
+ description: 'Dictionary entries in the selected organization with their stored base labels',
76
+ schema: createPagedListResponseSchema(z.object({
77
+ id: z.string().uuid(), kind: z.string(), value: z.string(), label: z.string(), updatedAt: z.string(),
78
+ })),
79
+ }],
80
+ },
81
+ },
82
+ }