@open-mercato/core 0.6.1-develop.3256.1.fe3dec2464 → 0.6.1-develop.3260.1.dc719cac84

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 (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/authFixtures.js.map +1 -1
  3. package/dist/helpers/integration/authUi.js +10 -2
  4. package/dist/helpers/integration/authUi.js.map +2 -2
  5. package/dist/helpers/integration/salesUi.js +3 -1
  6. package/dist/helpers/integration/salesUi.js.map +2 -2
  7. package/dist/modules/api_docs/frontend/docs/api/Explorer.js +10 -1
  8. package/dist/modules/api_docs/frontend/docs/api/Explorer.js.map +2 -2
  9. package/dist/modules/audit_logs/lib/display-helpers.js +28 -27
  10. package/dist/modules/audit_logs/lib/display-helpers.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +21 -3
  12. package/dist/modules/auth/api/users/route.js.map +2 -2
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js +6 -1
  14. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  15. package/dist/modules/auth/backend/users/create/page.js +4 -1
  16. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  17. package/dist/modules/auth/backend/users/page.js +56 -2
  18. package/dist/modules/auth/backend/users/page.js.map +3 -3
  19. package/dist/modules/auth/commands/users.js +14 -0
  20. package/dist/modules/auth/commands/users.js.map +2 -2
  21. package/dist/modules/auth/data/validators.js +8 -0
  22. package/dist/modules/auth/data/validators.js.map +2 -2
  23. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
  24. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -9
  26. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +26 -3
  28. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  29. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
  30. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  31. package/dist/modules/customers/components/PipelineSettings.js +108 -99
  32. package/dist/modules/customers/components/PipelineSettings.js.map +2 -2
  33. package/dist/modules/customers/components/detail/AssignRoleDialog.js +13 -5
  34. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  35. package/dist/modules/customers/components/detail/EntityTagsDialog.js +10 -2
  36. package/dist/modules/customers/components/detail/EntityTagsDialog.js.map +2 -2
  37. package/dist/modules/customers/components/detail/PersonCompaniesSection.js +15 -8
  38. package/dist/modules/customers/components/detail/PersonCompaniesSection.js.map +2 -2
  39. package/dist/modules/customers/components/detail/RolesSection.js +35 -22
  40. package/dist/modules/customers/components/detail/RolesSection.js.map +2 -2
  41. package/dist/modules/customers/components/detail/TasksSection.js +2 -2
  42. package/dist/modules/customers/components/detail/TasksSection.js.map +2 -2
  43. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +73 -64
  44. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  45. package/dist/modules/customers/components/linking/LinkEntityDialog.js +11 -2
  46. package/dist/modules/customers/components/linking/LinkEntityDialog.js.map +2 -2
  47. package/dist/modules/data_sync/backend/data-sync/page.js +2 -9
  48. package/dist/modules/data_sync/backend/data-sync/page.js.map +2 -2
  49. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js +14 -29
  50. package/dist/modules/data_sync/backend/data-sync/runs/[id]/page.js.map +2 -2
  51. package/dist/modules/dictionaries/components/DictionariesManager.js +10 -2
  52. package/dist/modules/dictionaries/components/DictionariesManager.js.map +2 -2
  53. package/dist/modules/integrations/backend/integrations/[id]/page.js +79 -95
  54. package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
  55. package/dist/modules/payment_gateways/backend/payment-gateways/page.js +49 -70
  56. package/dist/modules/payment_gateways/backend/payment-gateways/page.js.map +2 -2
  57. package/dist/modules/planner/components/AvailabilityRuleSetForm.js +1 -1
  58. package/dist/modules/planner/components/AvailabilityRuleSetForm.js.map +1 -1
  59. package/dist/modules/planner/components/AvailabilityRulesEditor.js +19 -19
  60. package/dist/modules/planner/components/AvailabilityRulesEditor.js.map +2 -2
  61. package/dist/modules/resources/components/ResourceCrudForm.js +1 -1
  62. package/dist/modules/resources/components/ResourceCrudForm.js.map +1 -1
  63. package/dist/modules/resources/components/ResourceTypeCrudForm.js +1 -1
  64. package/dist/modules/resources/components/ResourceTypeCrudForm.js.map +2 -2
  65. package/dist/modules/sales/components/documents/ReturnDialog.js +11 -8
  66. package/dist/modules/sales/components/documents/ReturnDialog.js.map +2 -2
  67. package/dist/modules/staff/backend/staff/profile/create/page.js +1 -1
  68. package/dist/modules/staff/backend/staff/profile/create/page.js.map +2 -2
  69. package/dist/modules/staff/components/TeamForm.js +1 -1
  70. package/dist/modules/staff/components/TeamForm.js.map +2 -2
  71. package/dist/modules/staff/components/TeamMemberForm.js +1 -1
  72. package/dist/modules/staff/components/TeamMemberForm.js.map +1 -1
  73. package/dist/modules/staff/components/TeamRoleForm.js +1 -1
  74. package/dist/modules/staff/components/TeamRoleForm.js.map +2 -2
  75. package/package.json +5 -5
  76. package/src/helpers/integration/authFixtures.ts +1 -1
  77. package/src/helpers/integration/authUi.ts +11 -2
  78. package/src/helpers/integration/salesUi.ts +10 -1
  79. package/src/modules/api_docs/frontend/docs/api/Explorer.tsx +7 -3
  80. package/src/modules/audit_logs/lib/display-helpers.tsx +22 -14
  81. package/src/modules/auth/api/users/route.ts +22 -3
  82. package/src/modules/auth/backend/users/[id]/edit/page.tsx +9 -1
  83. package/src/modules/auth/backend/users/create/page.tsx +6 -2
  84. package/src/modules/auth/backend/users/page.tsx +60 -3
  85. package/src/modules/auth/commands/users.ts +15 -0
  86. package/src/modules/auth/data/validators.ts +8 -0
  87. package/src/modules/auth/i18n/de.json +4 -0
  88. package/src/modules/auth/i18n/en.json +4 -0
  89. package/src/modules/auth/i18n/es.json +4 -0
  90. package/src/modules/auth/i18n/pl.json +4 -0
  91. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
  92. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +12 -13
  93. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +17 -13
  94. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
  95. package/src/modules/customers/components/PipelineSettings.tsx +95 -95
  96. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +7 -4
  97. package/src/modules/customers/components/detail/EntityTagsDialog.tsx +13 -8
  98. package/src/modules/customers/components/detail/PersonCompaniesSection.tsx +16 -12
  99. package/src/modules/customers/components/detail/RolesSection.tsx +34 -31
  100. package/src/modules/customers/components/detail/TasksSection.tsx +16 -8
  101. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +77 -51
  102. package/src/modules/customers/components/linking/LinkEntityDialog.tsx +7 -3
  103. package/src/modules/data_sync/backend/data-sync/page.tsx +4 -7
  104. package/src/modules/data_sync/backend/data-sync/runs/[id]/page.tsx +19 -54
  105. package/src/modules/dictionaries/components/DictionariesManager.tsx +7 -4
  106. package/src/modules/integrations/backend/integrations/[id]/page.tsx +101 -146
  107. package/src/modules/payment_gateways/backend/payment-gateways/page.tsx +60 -103
  108. package/src/modules/planner/components/AvailabilityRuleSetForm.tsx +1 -1
  109. package/src/modules/planner/components/AvailabilityRulesEditor.tsx +17 -17
  110. package/src/modules/resources/components/ResourceCrudForm.tsx +1 -1
  111. package/src/modules/resources/components/ResourceTypeCrudForm.tsx +1 -1
  112. package/src/modules/sales/components/documents/ReturnDialog.tsx +10 -7
  113. package/src/modules/sales/i18n/de.json +2 -0
  114. package/src/modules/sales/i18n/en.json +2 -0
  115. package/src/modules/sales/i18n/es.json +2 -0
  116. package/src/modules/sales/i18n/pl.json +2 -0
  117. package/src/modules/staff/backend/staff/profile/create/page.tsx +1 -1
  118. package/src/modules/staff/components/TeamForm.tsx +1 -1
  119. package/src/modules/staff/components/TeamMemberForm.tsx +1 -1
  120. package/src/modules/staff/components/TeamRoleForm.tsx +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/staff/components/TeamRoleForm.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { normalizeCustomFieldValues } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { AppearanceSelector } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { E } from '#generated/entities.ids.generated'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TeamRoleFormValues = {\n id?: string\n teamId?: string | null\n name: string\n description?: string | null\n appearance?: { icon?: string | null; color?: string | null }\n} & Record<string, unknown>\n\nexport type TeamRoleOption = {\n id: string\n name: string\n}\n\nexport type TeamRoleFormProps = {\n title: string\n submitLabel?: string\n backHref: string\n cancelHref: string\n initialValues: TeamRoleFormValues\n teamOptions?: TeamRoleOption[]\n onSubmit: (values: TeamRoleFormValues) => Promise<void>\n onDelete?: () => Promise<void>\n isLoading?: boolean\n loadingMessage?: string\n extraActions?: React.ReactNode\n}\n\nconst normalizeCustomFieldSubmitValue = (value: unknown): unknown => {\n const normalized = normalizeCustomFieldValues({ value })\n return normalized.value\n}\n\nexport const buildTeamRolePayload = (\n values: TeamRoleFormValues,\n options: { id?: string } = {},\n): Record<string, unknown> => {\n const name = typeof values.name === 'string' ? values.name.trim() : ''\n const description = typeof values.description === 'string' && values.description.trim().length\n ? values.description.trim()\n : null\n const teamId = typeof values.teamId === 'string' && values.teamId.trim().length ? values.teamId : null\n const appearance = values.appearance && typeof values.appearance === 'object'\n ? values.appearance as { icon?: string | null; color?: string | null }\n : {}\n const customFields = collectCustomFieldValues(values, { transform: normalizeCustomFieldSubmitValue })\n return {\n ...(options.id ? { id: options.id } : {}),\n teamId,\n name,\n description,\n appearanceIcon: appearance.icon ?? null,\n appearanceColor: appearance.color ?? null,\n ...(Object.keys(customFields).length ? { customFields } : {}),\n }\n}\n\nexport function TeamRoleForm(props: TeamRoleFormProps) {\n const {\n title,\n submitLabel,\n backHref,\n cancelHref,\n initialValues,\n teamOptions,\n onSubmit,\n onDelete,\n isLoading,\n loadingMessage,\n extraActions,\n } = props\n const t = useT()\n\n const appearanceLabels = React.useMemo(() => ({\n colorLabel: t('staff.teamRoles.form.appearance.colorLabel', 'Color'),\n colorHelp: t('staff.teamRoles.form.appearance.colorHelp', 'Pick a color for this team role.'),\n colorClearLabel: t('staff.teamRoles.form.appearance.colorClear', 'Clear color'),\n iconLabel: t('staff.teamRoles.form.appearance.iconLabel', 'Icon'),\n iconPlaceholder: t('staff.teamRoles.form.appearance.iconPlaceholder', 'Type an emoji or icon name'),\n iconPickerTriggerLabel: t('staff.teamRoles.form.appearance.iconPicker', 'Browse icons'),\n iconSearchPlaceholder: t('staff.teamRoles.form.appearance.iconSearch', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('staff.teamRoles.form.appearance.iconSearchEmpty', 'No icons match your search'),\n iconSuggestionsLabel: t('staff.teamRoles.form.appearance.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('staff.teamRoles.form.appearance.iconClear', 'Clear icon'),\n previewEmptyLabel: t('staff.teamRoles.form.appearance.previewEmpty', 'No appearance selected'),\n }), [t])\n\n const fields = React.useMemo<CrudField[]>(() => {\n const base: CrudField[] = []\n if (teamOptions && teamOptions.length) {\n base.push({\n id: 'teamId',\n label: t('staff.teamRoles.form.fields.team', 'Team'),\n type: 'select',\n listbox: true,\n options: [\n { value: '', label: t('staff.teamRoles.form.fields.team.unassigned', 'Unassigned') },\n ...teamOptions.map((team) => ({ value: team.id, label: team.name })),\n ],\n })\n }\n base.push(\n { id: 'name', label: t('staff.teamRoles.form.fields.name', 'Name'), type: 'text', required: true },\n { id: 'description', label: t('staff.teamRoles.form.fields.description', 'Description'), type: 'richtext' },\n {\n id: 'appearance',\n label: t('staff.teamRoles.form.appearance.label', 'Appearance'),\n type: 'custom',\n component: ({ value, setValue }) => {\n const current = value && typeof value === 'object'\n ? (value as { icon?: string | null; color?: string | null })\n : {}\n return (\n <AppearanceSelector\n icon={current.icon ?? null}\n color={current.color ?? null}\n onIconChange={(next) => setValue({ ...current, icon: next })}\n onColorChange={(next) => setValue({ ...current, color: next })}\n labels={appearanceLabels}\n />\n )\n },\n },\n )\n return base\n }, [appearanceLabels, t, teamOptions])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => [\n { id: 'details', fields: teamOptions && teamOptions.length ? ['teamId', 'name', 'description', 'appearance'] : ['name', 'description', 'appearance'] },\n { id: 'custom', title: t('entities.customFields.title', 'Custom Attributes'), column: 2, kind: 'customFields' },\n ], [t, teamOptions])\n\n return (\n <CrudForm<TeamRoleFormValues>\n title={title}\n backHref={backHref}\n cancelHref={cancelHref}\n versionHistory={initialValues.id\n ? { resourceKind: 'staff.teamRole', resourceId: String(initialValues.id) }\n : undefined}\n submitLabel={submitLabel}\n fields={fields}\n groups={groups}\n entityId={E.staff.staff_team_role}\n initialValues={initialValues}\n onSubmit={onSubmit}\n onDelete={onDelete}\n isLoading={isLoading}\n loadingMessage={loadingMessage}\n extraActions={extraActions}\n />\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'\nimport { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'\nimport { normalizeCustomFieldValues } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { AppearanceSelector } from '@open-mercato/core/modules/dictionaries/components/AppearanceSelector'\nimport { E } from '#generated/entities.ids.generated'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\n\nexport type TeamRoleFormValues = {\n id?: string\n teamId?: string | null\n name: string\n description?: string | null\n appearance?: { icon?: string | null; color?: string | null }\n} & Record<string, unknown>\n\nexport type TeamRoleOption = {\n id: string\n name: string\n}\n\nexport type TeamRoleFormProps = {\n title: string\n submitLabel?: string\n backHref: string\n cancelHref: string\n initialValues: TeamRoleFormValues\n teamOptions?: TeamRoleOption[]\n onSubmit: (values: TeamRoleFormValues) => Promise<void>\n onDelete?: () => Promise<void>\n isLoading?: boolean\n loadingMessage?: string\n extraActions?: React.ReactNode\n}\n\nconst normalizeCustomFieldSubmitValue = (value: unknown): unknown => {\n const normalized = normalizeCustomFieldValues({ value })\n return normalized.value\n}\n\nexport const buildTeamRolePayload = (\n values: TeamRoleFormValues,\n options: { id?: string } = {},\n): Record<string, unknown> => {\n const name = typeof values.name === 'string' ? values.name.trim() : ''\n const description = typeof values.description === 'string' && values.description.trim().length\n ? values.description.trim()\n : null\n const teamId = typeof values.teamId === 'string' && values.teamId.trim().length ? values.teamId : null\n const appearance = values.appearance && typeof values.appearance === 'object'\n ? values.appearance as { icon?: string | null; color?: string | null }\n : {}\n const customFields = collectCustomFieldValues(values, { transform: normalizeCustomFieldSubmitValue })\n return {\n ...(options.id ? { id: options.id } : {}),\n teamId,\n name,\n description,\n appearanceIcon: appearance.icon ?? null,\n appearanceColor: appearance.color ?? null,\n ...(Object.keys(customFields).length ? { customFields } : {}),\n }\n}\n\nexport function TeamRoleForm(props: TeamRoleFormProps) {\n const {\n title,\n submitLabel,\n backHref,\n cancelHref,\n initialValues,\n teamOptions,\n onSubmit,\n onDelete,\n isLoading,\n loadingMessage,\n extraActions,\n } = props\n const t = useT()\n\n const appearanceLabels = React.useMemo(() => ({\n colorLabel: t('staff.teamRoles.form.appearance.colorLabel', 'Color'),\n colorHelp: t('staff.teamRoles.form.appearance.colorHelp', 'Pick a color for this team role.'),\n colorClearLabel: t('staff.teamRoles.form.appearance.colorClear', 'Clear color'),\n iconLabel: t('staff.teamRoles.form.appearance.iconLabel', 'Icon'),\n iconPlaceholder: t('staff.teamRoles.form.appearance.iconPlaceholder', 'Type an emoji or icon name'),\n iconPickerTriggerLabel: t('staff.teamRoles.form.appearance.iconPicker', 'Browse icons'),\n iconSearchPlaceholder: t('staff.teamRoles.form.appearance.iconSearch', 'Search icons or emojis\u2026'),\n iconSearchEmptyLabel: t('staff.teamRoles.form.appearance.iconSearchEmpty', 'No icons match your search'),\n iconSuggestionsLabel: t('staff.teamRoles.form.appearance.iconSuggestions', 'Suggestions'),\n iconClearLabel: t('staff.teamRoles.form.appearance.iconClear', 'Clear icon'),\n previewEmptyLabel: t('staff.teamRoles.form.appearance.previewEmpty', 'No appearance selected'),\n }), [t])\n\n const fields = React.useMemo<CrudField[]>(() => {\n const base: CrudField[] = []\n if (teamOptions && teamOptions.length) {\n base.push({\n id: 'teamId',\n label: t('staff.teamRoles.form.fields.team', 'Team'),\n type: 'select',\n listbox: true,\n options: [\n { value: '', label: t('staff.teamRoles.form.fields.team.unassigned', 'Unassigned') },\n ...teamOptions.map((team) => ({ value: team.id, label: team.name })),\n ],\n })\n }\n base.push(\n { id: 'name', label: t('staff.teamRoles.form.fields.name', 'Name'), type: 'text', required: true },\n { id: 'description', label: t('staff.teamRoles.form.fields.description', 'Description'), type: 'richtext', editor: 'html' },\n {\n id: 'appearance',\n label: t('staff.teamRoles.form.appearance.label', 'Appearance'),\n type: 'custom',\n component: ({ value, setValue }) => {\n const current = value && typeof value === 'object'\n ? (value as { icon?: string | null; color?: string | null })\n : {}\n return (\n <AppearanceSelector\n icon={current.icon ?? null}\n color={current.color ?? null}\n onIconChange={(next) => setValue({ ...current, icon: next })}\n onColorChange={(next) => setValue({ ...current, color: next })}\n labels={appearanceLabels}\n />\n )\n },\n },\n )\n return base\n }, [appearanceLabels, t, teamOptions])\n\n const groups = React.useMemo<CrudFormGroup[]>(() => [\n { id: 'details', fields: teamOptions && teamOptions.length ? ['teamId', 'name', 'description', 'appearance'] : ['name', 'description', 'appearance'] },\n { id: 'custom', title: t('entities.customFields.title', 'Custom Attributes'), column: 2, kind: 'customFields' },\n ], [t, teamOptions])\n\n return (\n <CrudForm<TeamRoleFormValues>\n title={title}\n backHref={backHref}\n cancelHref={cancelHref}\n versionHistory={initialValues.id\n ? { resourceKind: 'staff.teamRole', resourceId: String(initialValues.id) }\n : undefined}\n submitLabel={submitLabel}\n fields={fields}\n groups={groups}\n entityId={E.staff.staff_team_role}\n initialValues={initialValues}\n onSubmit={onSubmit}\n onDelete={onDelete}\n isLoading={isLoading}\n loadingMessage={loadingMessage}\n extraActions={extraActions}\n />\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.1-develop.3256.1.fe3dec2464",
3
+ "version": "0.6.1-develop.3260.1.dc719cac84",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -237,12 +237,12 @@
237
237
  "ts-pattern": "^5.0.0"
238
238
  },
239
239
  "peerDependencies": {
240
- "@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
241
- "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464"
240
+ "@open-mercato/ai-assistant": "0.6.1-develop.3260.1.dc719cac84",
241
+ "@open-mercato/shared": "0.6.1-develop.3260.1.dc719cac84"
242
242
  },
243
243
  "devDependencies": {
244
- "@open-mercato/ai-assistant": "0.6.1-develop.3256.1.fe3dec2464",
245
- "@open-mercato/shared": "0.6.1-develop.3256.1.fe3dec2464",
244
+ "@open-mercato/ai-assistant": "0.6.1-develop.3260.1.dc719cac84",
245
+ "@open-mercato/shared": "0.6.1-develop.3260.1.dc719cac84",
246
246
  "@testing-library/dom": "^10.4.1",
247
247
  "@testing-library/jest-dom": "^6.9.1",
248
248
  "@testing-library/react": "^16.3.1",
@@ -34,7 +34,7 @@ export async function deleteRoleIfExists(
34
34
  export async function createUserFixture(
35
35
  request: APIRequestContext,
36
36
  token: string,
37
- input: { email: string; password: string; organizationId: string; roles: string[] },
37
+ input: { email: string; password: string; organizationId: string; roles: string[]; name?: string },
38
38
  ): Promise<string> {
39
39
  const response = await apiRequest(request, 'POST', '/api/auth/users', {
40
40
  token,
@@ -6,8 +6,17 @@ export async function createUserViaUi(page: Page, input: { email: string; passwo
6
6
  await page.goto('/backend/users/create');
7
7
  await expect(page.getByText('Create User')).toBeVisible();
8
8
 
9
- await page.getByRole('textbox').nth(0).fill(input.email);
10
- await page.getByRole('textbox').nth(1).fill(input.password);
9
+ const emailInput = page.locator('[data-crud-field-id="email"] input').first();
10
+ const nameInput = page.locator('[data-crud-field-id="name"] input').first();
11
+ const passwordInput = page.locator('[data-crud-field-id="password"] input').first();
12
+
13
+ await expect(emailInput).toBeVisible();
14
+ await emailInput.fill(input.email);
15
+ if (await nameInput.count()) {
16
+ await nameInput.fill('');
17
+ }
18
+ await expect(passwordInput).toBeVisible();
19
+ await passwordInput.fill(input.password);
11
20
 
12
21
  const orgSelect = page.locator('main').locator('select').first();
13
22
  await expect(orgSelect).toBeEnabled();
@@ -1090,13 +1090,22 @@ export async function addAdjustment(page: Page, options: AddAdjustmentOptions):
1090
1090
  const adjustmentRow = page.getByRole('row', { name: new RegExp(escapeRegExp(options.label), 'i') });
1091
1091
  const fillAdjustmentForm = async (): Promise<void> => {
1092
1092
  await dialog.getByText(/Loading adjustments/i).waitFor({ state: 'hidden', timeout: 3_000 }).catch(() => {});
1093
+ // AdjustmentDialog has a useEffect([initialAdjustment, kindOptions,
1094
+ // loadKindOptions, loadTaxRates, open]) that asynchronously loads tax
1095
+ // rates and resets the form via setInitialValues + a formResetKey bump.
1096
+ // If `.fill()` lands before the reset settles, the label is wiped out.
1097
+ // Wait for the tax-rates network response to land before touching the form.
1098
+ await page.waitForLoadState('networkidle', { timeout: 5_000 }).catch(() => {});
1093
1099
  // Radix Select trigger — target by CrudForm field id (kind picker)
1094
1100
  const kindTrigger = dialog.locator('[data-crud-field-id="kind"] [role="combobox"]').first();
1095
1101
  await expect(kindTrigger).toBeVisible({ timeout: TEST_WAIT_TIMEOUT_MS });
1096
1102
 
1097
1103
  const labelInput = dialog.getByPlaceholder(/e\.g\. Shipping fee/i).first();
1098
1104
  await expect(labelInput).toBeVisible({ timeout: TEST_WAIT_TIMEOUT_MS });
1099
- await fillControlledInput(labelInput, options.label);
1105
+ // Bumped toHaveValue timeout 2 s → 5 s — covers the worst-case reset race
1106
+ // where the tax-rates fetch resolves *after* the networkidle window above
1107
+ // (e.g. when a slow Docker host bundles the requests inconsistently).
1108
+ await fillControlledInput(labelInput, options.label, 5_000);
1100
1109
 
1101
1110
  if ((await kindTrigger.count()) > 0) {
1102
1111
  const expectedKindValue = normalizeAdjustmentKindValue(options.kindLabel ?? 'Surcharge');
@@ -3,7 +3,9 @@
3
3
  import { useEffect, useMemo, useState, type ReactNode } from 'react'
4
4
  import Image from 'next/image'
5
5
  import Link from 'next/link'
6
+ import { SearchX } from 'lucide-react'
6
7
  import { Input } from '@open-mercato/ui/primitives/input'
8
+ import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
7
9
  import {
8
10
  Select,
9
11
  SelectContent,
@@ -781,9 +783,11 @@ export default function ApiDocsExplorer(props: ApiDocsExplorerProps) {
781
783
  )
782
784
  })}
783
785
  {!categories.length ? (
784
- <div className="rounded-lg border border-dashed bg-muted/50 p-6 text-center text-sm text-muted-foreground">
785
- No endpoints match your filters. Try adjusting the method filters or clearing the search query.
786
- </div>
786
+ <EmptyState
787
+ icon={<SearchX className="h-8 w-8" aria-hidden="true" />}
788
+ title="No endpoints match your filters."
789
+ description="Try adjusting the method filters or clearing the search query."
790
+ />
787
791
  ) : null}
788
792
  </div>
789
793
  </section>
@@ -1,5 +1,11 @@
1
1
  import * as React from 'react'
2
2
  import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
3
+ import {
4
+ Accordion,
5
+ AccordionContent,
6
+ AccordionItem,
7
+ AccordionTrigger,
8
+ } from '@open-mercato/ui/primitives/accordion'
3
9
  import type { ChangeRow } from './changeRows'
4
10
  import { isRecord } from './changeRows'
5
11
  export { extractChangeRows, isRecord } from './changeRows'
@@ -125,25 +131,27 @@ export type CollapsibleJsonSectionProps = {
125
131
 
126
132
  const DEFAULT_TRUNCATE_AT = 5000
127
133
 
134
+ const COLLAPSIBLE_JSON_ITEM_VALUE = 'open'
135
+
128
136
  export function CollapsibleJsonSection({ label, value, truncateAt = DEFAULT_TRUNCATE_AT }: CollapsibleJsonSectionProps) {
129
- const [isOpen, setIsOpen] = React.useState(false)
137
+ const [openValue, setOpenValue] = React.useState<string>('')
130
138
  const [showFull, setShowFull] = React.useState(false)
131
139
 
140
+ const isOpen = openValue === COLLAPSIBLE_JSON_ITEM_VALUE
132
141
  const stringified = React.useMemo(() => (isOpen ? safeStringify(value) : ''), [isOpen, value])
133
142
  const isTruncated = stringified.length > truncateAt
134
143
  const displayText = !showFull && isTruncated ? stringified.slice(0, truncateAt) : stringified
135
144
 
136
145
  return (
137
- <details
138
- className="group rounded-lg border px-4 py-3"
139
- onToggle={(event) => setIsOpen((event.target as HTMLDetailsElement).open)}
140
- >
141
- <summary className="cursor-pointer text-sm font-semibold text-foreground transition-colors group-open:text-primary">
142
- {label}
143
- </summary>
144
- {isOpen ? (
145
- <>
146
- <pre className="mt-2 max-h-96 overflow-auto whitespace-pre-wrap break-words text-xs leading-5 text-muted-foreground">
146
+ <Accordion type="single" collapsible value={openValue} onValueChange={setOpenValue}>
147
+ <AccordionItem value={COLLAPSIBLE_JSON_ITEM_VALUE}>
148
+ <AccordionTrigger>
149
+ <span className="font-semibold text-foreground group-data-[state=open]/accordion-trigger:text-primary">
150
+ {label}
151
+ </span>
152
+ </AccordionTrigger>
153
+ <AccordionContent>
154
+ <pre className="max-h-96 overflow-auto whitespace-pre-wrap break-words text-xs leading-5 text-muted-foreground">
147
155
  {displayText}
148
156
  {!showFull && isTruncated ? '\n…' : null}
149
157
  </pre>
@@ -156,8 +164,8 @@ export function CollapsibleJsonSection({ label, value, truncateAt = DEFAULT_TRUN
156
164
  {showFull ? 'Show less' : `Show all (${Math.ceil(stringified.length / 1024)} KB)`}
157
165
  </button>
158
166
  ) : null}
159
- </>
160
- ) : null}
161
- </details>
167
+ </AccordionContent>
168
+ </AccordionItem>
169
+ </Accordion>
162
170
  )
163
171
  }
@@ -26,6 +26,7 @@ const querySchema = z.object({
26
26
  page: z.coerce.number().min(1).default(1),
27
27
  pageSize: z.coerce.number().min(1).max(100).default(50),
28
28
  search: z.string().optional(),
29
+ name: z.string().optional(),
29
30
  organizationId: z.string().uuid().optional(),
30
31
  roleIds: z.array(z.string().uuid()).optional(),
31
32
  }).passthrough()
@@ -34,8 +35,18 @@ const rawBodySchema = z.object({}).passthrough()
34
35
 
35
36
  const passwordSchema = buildPasswordSchema()
36
37
 
38
+ const displayNameSchema = z.preprocess(
39
+ (value) => {
40
+ if (typeof value !== 'string') return value
41
+ const trimmed = value.trim()
42
+ return trimmed.length ? trimmed : undefined
43
+ },
44
+ z.string().trim().min(1).max(120).optional(),
45
+ )
46
+
37
47
  const userCreateSchema = z.object({
38
48
  email: z.string().email(),
49
+ name: displayNameSchema,
39
50
  password: passwordSchema.optional(),
40
51
  sendInviteEmail: z.boolean().optional(),
41
52
  organizationId: z.string().uuid(),
@@ -48,6 +59,7 @@ const userCreateSchema = z.object({
48
59
  const userUpdateSchema = z.object({
49
60
  id: z.string().uuid(),
50
61
  email: z.string().email().optional(),
62
+ name: displayNameSchema,
51
63
  password: passwordSchema.optional(),
52
64
  organizationId: z.string().uuid().optional(),
53
65
  roles: z.array(z.string()).optional(),
@@ -56,6 +68,7 @@ const userUpdateSchema = z.object({
56
68
  const userListItemSchema = z.object({
57
69
  id: z.string().uuid(),
58
70
  email: z.string().email(),
71
+ name: z.string().nullable(),
59
72
  organizationId: z.string().uuid().nullable(),
60
73
  organizationName: z.string().nullable(),
61
74
  tenantId: z.string().uuid().nullable(),
@@ -141,6 +154,7 @@ export async function GET(req: Request) {
141
154
  page: url.searchParams.get('page') || undefined,
142
155
  pageSize: url.searchParams.get('pageSize') || undefined,
143
156
  search: url.searchParams.get('search') || undefined,
157
+ name: url.searchParams.get('name') || undefined,
144
158
  organizationId: url.searchParams.get('organizationId') || undefined,
145
159
  roleIds: rawRoleIds.length ? rawRoleIds : undefined,
146
160
  })
@@ -157,7 +171,7 @@ export async function GET(req: Request) {
157
171
  } catch (err) {
158
172
  console.error('users: failed to resolve rbac', err)
159
173
  }
160
- const { id, page, pageSize, search, organizationId, roleIds } = parsed.data
174
+ const { id, page, pageSize, search, name, organizationId, roleIds } = parsed.data
161
175
  const filters: any[] = [{ deletedAt: null }]
162
176
  const actorTenantId = auth.tenantId ? String(auth.tenantId) : null
163
177
  if (!isSuperAdmin) {
@@ -167,6 +181,10 @@ export async function GET(req: Request) {
167
181
  filters.push({ tenantId: actorTenantId })
168
182
  }
169
183
  if (organizationId) filters.push({ organizationId })
184
+ const trimmedName = typeof name === 'string' ? name.trim() : ''
185
+ if (trimmedName) {
186
+ filters.push({ name: { $ilike: `%${escapeLikePattern(trimmedName)}%` } })
187
+ }
170
188
  let idFilter: Set<string> | null = id ? new Set([id]) : null
171
189
  if (Array.isArray(roleIds) && roleIds.length > 0) {
172
190
  const uniqueRoleIds = Array.from(new Set(roleIds))
@@ -345,6 +363,7 @@ export async function GET(req: Request) {
345
363
  return {
346
364
  id: uid,
347
365
  email: String(u.email),
366
+ name: u.name ? String(u.name) : null,
348
367
  organizationId: orgId,
349
368
  organizationName: orgId ? orgMap[orgId] ?? orgId : null,
350
369
  tenantId: u.tenantId ? String(u.tenantId) : null,
@@ -475,7 +494,7 @@ export const openApi: OpenApiRouteDoc = {
475
494
  },
476
495
  POST: {
477
496
  summary: 'Create user',
478
- description: 'Creates a new confirmed user within the specified organization and optional roles.',
497
+ description: 'Creates a new confirmed user within the specified organization, optional display name, and optional roles.',
479
498
  requestBody: {
480
499
  contentType: 'application/json',
481
500
  schema: userCreateSchema,
@@ -495,7 +514,7 @@ export const openApi: OpenApiRouteDoc = {
495
514
  },
496
515
  PUT: {
497
516
  summary: 'Update user',
498
- description: 'Updates profile fields, organization assignment, credentials, or role memberships.',
517
+ description: 'Updates profile fields including display name, organization assignment, credentials, or role memberships.',
499
518
  requestBody: {
500
519
  contentType: 'application/json',
501
520
  schema: userUpdateSchema,
@@ -19,6 +19,7 @@ import { UserConsentsPanel } from '@open-mercato/core/modules/auth/components/Us
19
19
 
20
20
  type EditUserFormValues = {
21
21
  email: string
22
+ name: string
22
23
  password: string
23
24
  tenantId: string | null
24
25
  organizationId: string | null
@@ -28,6 +29,7 @@ type EditUserFormValues = {
28
29
  type LoadedUser = {
29
30
  id: string
30
31
  email: string
32
+ name: string | null
31
33
  organizationId: string | null
32
34
  tenantId: string | null
33
35
  tenantName: string | null
@@ -40,6 +42,7 @@ type LoadedUser = {
40
42
  type UserApiItem = {
41
43
  id?: string | null
42
44
  email?: string | null
45
+ name?: string | null
43
46
  organizationId?: string | null
44
47
  tenantId?: string | null
45
48
  tenantName?: string | null
@@ -193,6 +196,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
193
196
  setInitialUser({
194
197
  id: item.id ? String(item.id) : String(id),
195
198
  email: item.email ? String(item.email) : '',
199
+ name: item.name ? String(item.name) : null,
196
200
  organizationId: item.organizationId ? String(item.organizationId) : null,
197
201
  tenantId: item.tenantId ? String(item.tenantId) : null,
198
202
  tenantName: item.tenantName ? String(item.tenantName) : null,
@@ -261,6 +265,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
261
265
  const fields: CrudField[] = React.useMemo(() => {
262
266
  const items: CrudField[] = [
263
267
  { id: 'email', label: t('auth.users.form.field.email', 'Email'), type: 'text', required: true },
268
+ { id: 'name', label: t('auth.users.form.field.name', 'Display name'), type: 'text' },
264
269
  {
265
270
  id: 'password',
266
271
  label: userHasPassword
@@ -327,7 +332,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
327
332
  }, [actorIsSuperAdmin, loadRoleOptions, passwordDescription, preloadedTenants, selectedOrgId, selectedTenantId, t, userHasPassword])
328
333
 
329
334
  const detailFieldIds = React.useMemo(() => {
330
- const base: string[] = ['email', 'password', 'organizationId', 'roles']
335
+ const base: string[] = ['email', 'name', 'password', 'organizationId', 'roles']
331
336
  if (actorIsSuperAdmin) base.splice(2, 0, 'tenantId')
332
337
  return base
333
338
  }, [actorIsSuperAdmin])
@@ -382,6 +387,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
382
387
  if (initialUser) {
383
388
  return {
384
389
  email: initialUser.email,
390
+ name: initialUser.name ?? '',
385
391
  password: '',
386
392
  tenantId: initialUser.tenantId,
387
393
  organizationId: initialUser.organizationId,
@@ -391,6 +397,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
391
397
  }
392
398
  return {
393
399
  email: '',
400
+ name: '',
394
401
  password: '',
395
402
  tenantId: selectedTenantId ?? null,
396
403
  organizationId: null,
@@ -438,6 +445,7 @@ export default function EditUserPage({ params }: { params?: { id?: string } }) {
438
445
  const payload = {
439
446
  id: id ? String(id) : '',
440
447
  email: values.email,
448
+ name: typeof values.name === 'string' && values.name.trim().length ? values.name.trim() : undefined,
441
449
  password: values.password && values.password.trim() ? values.password : undefined,
442
450
  organizationId: values.organizationId ? values.organizationId : undefined,
443
451
  roles: Array.isArray(values.roles) ? values.roles : [],
@@ -17,6 +17,7 @@ import { formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/sha
17
17
 
18
18
  type CreateUserFormValues = {
19
19
  email: string
20
+ name: string
20
21
  password: string
21
22
  tenantId: string | null
22
23
  organizationId: string | null
@@ -177,6 +178,7 @@ export default function CreateUserPage() {
177
178
  const fields: CrudField[] = React.useMemo(() => {
178
179
  const items: CrudField[] = [
179
180
  { id: 'email', label: t('auth.users.form.field.email', 'Email'), type: 'text', required: true },
181
+ { id: 'name', label: t('auth.users.form.field.name', 'Display name'), type: 'text' },
180
182
  {
181
183
  id: 'sendInviteEmail',
182
184
  label: t('auth.users.form.field.sendInviteEmail', 'Send password setup link via email'),
@@ -251,8 +253,8 @@ export default function CreateUserPage() {
251
253
 
252
254
  const detailFieldIds = React.useMemo(() => {
253
255
  const base: string[] = sendInviteEmail
254
- ? ['email', 'sendInviteEmail', 'organizationId', 'roles']
255
- : ['email', 'sendInviteEmail', 'password', 'organizationId', 'roles']
256
+ ? ['email', 'name', 'sendInviteEmail', 'organizationId', 'roles']
257
+ : ['email', 'name', 'sendInviteEmail', 'password', 'organizationId', 'roles']
256
258
  if (actorIsSuperAdmin) {
257
259
  const orgIdx = base.indexOf('organizationId')
258
260
  base.splice(orgIdx, 0, 'tenantId')
@@ -294,6 +296,7 @@ export default function CreateUserPage() {
294
296
  const initialValues = React.useMemo<Partial<CreateUserFormValues>>(
295
297
  () => ({
296
298
  email: '',
299
+ name: '',
297
300
  password: '',
298
301
  tenantId: null,
299
302
  organizationId: null,
@@ -323,6 +326,7 @@ export default function CreateUserPage() {
323
326
  const customFields = collectCustomFieldValues(values)
324
327
  const payload: Record<string, unknown> = {
325
328
  email: values.email,
329
+ name: typeof values.name === 'string' && values.name.trim().length ? values.name.trim() : undefined,
326
330
  organizationId: values.organizationId ? values.organizationId : null,
327
331
  roles: Array.isArray(values.roles) ? values.roles : [],
328
332
  ...(Object.keys(customFields).length ? { customFields } : {}),
@@ -3,7 +3,7 @@ import * as React from 'react'
3
3
  import Link from 'next/link'
4
4
  import { useSearchParams } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
- import { DataTable } from '@open-mercato/ui/backend/DataTable'
6
+ import { DataTable, type DataTableExportFormat } from '@open-mercato/ui/backend/DataTable'
7
7
  import type { ColumnDef, SortingState } from '@tanstack/react-table'
8
8
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
@@ -20,6 +20,7 @@ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
20
20
  type Row = {
21
21
  id: string
22
22
  email: string
23
+ name: string | null
23
24
  organizationId: string | null
24
25
  organizationName?: string | null
25
26
  tenantId: string | null
@@ -216,6 +217,12 @@ export default function UsersListPage() {
216
217
  }, [applyRoleOptions])
217
218
 
218
219
  const filters = React.useMemo<FilterDef[]>(() => [
220
+ {
221
+ id: 'name',
222
+ label: t('auth.users.list.filters.name', 'Display name'),
223
+ type: 'text',
224
+ placeholder: t('auth.users.list.filters.namePlaceholder', 'Filter by display name'),
225
+ },
219
226
  {
220
227
  id: 'organizationId',
221
228
  label: t('auth.users.list.filters.organization', 'Organization'),
@@ -258,6 +265,8 @@ export default function UsersListPage() {
258
265
 
259
266
  const handleFiltersApply = React.useCallback((values: FilterValues) => {
260
267
  const next: FilterValues = {}
268
+ const name = typeof values.name === 'string' ? values.name.trim() : ''
269
+ if (name) next.name = name
261
270
  const org = typeof values.organizationId === 'string' ? values.organizationId.trim() : ''
262
271
  if (org) next.organizationId = org
263
272
  const rawRoles = Array.isArray(values.roles) ? (values.roles as string[]) : []
@@ -274,17 +283,63 @@ export default function UsersListPage() {
274
283
  setPage(1)
275
284
  }, [])
276
285
 
286
+ const exportParams = React.useMemo(() => {
287
+ const name = typeof filterValues.name === 'string' ? filterValues.name.trim() : ''
288
+ return {
289
+ page,
290
+ pageSize: 50,
291
+ search,
292
+ name: name.length ? name : undefined,
293
+ organizationId,
294
+ roleIds: normalizedRoleIds,
295
+ }
296
+ }, [filterValues.name, normalizedRoleIds, organizationId, page, search])
297
+
298
+ const exportConfig = React.useMemo(() => ({
299
+ view: {
300
+ getUrl: (format: DataTableExportFormat) => {
301
+ const params = new URLSearchParams()
302
+ params.set('page', String(exportParams.page))
303
+ params.set('pageSize', String(exportParams.pageSize))
304
+ if (exportParams.search) params.set('search', exportParams.search)
305
+ if (exportParams.name) params.set('name', exportParams.name)
306
+ if (exportParams.organizationId) params.set('organizationId', exportParams.organizationId)
307
+ for (const roleId of exportParams.roleIds) params.append('roleId', roleId)
308
+ params.set('format', format)
309
+ params.set('exportScope', 'view')
310
+ return `/api/auth/users?${params.toString()}`
311
+ },
312
+ },
313
+ full: {
314
+ getUrl: (format: DataTableExportFormat) => {
315
+ const params = new URLSearchParams()
316
+ params.set('page', String(exportParams.page))
317
+ params.set('pageSize', String(exportParams.pageSize))
318
+ if (exportParams.search) params.set('search', exportParams.search)
319
+ if (exportParams.name) params.set('name', exportParams.name)
320
+ if (exportParams.organizationId) params.set('organizationId', exportParams.organizationId)
321
+ for (const roleId of exportParams.roleIds) params.append('roleId', roleId)
322
+ params.set('format', format)
323
+ params.set('exportScope', 'full')
324
+ params.set('all', 'true')
325
+ return `/api/auth/users?${params.toString()}`
326
+ },
327
+ },
328
+ }), [exportParams])
329
+
277
330
  const params = React.useMemo(() => {
278
331
  const p = new URLSearchParams()
279
332
  p.set('page', String(page))
280
333
  p.set('pageSize', '50')
281
334
  if (search) p.set('search', search)
335
+ const name = typeof filterValues.name === 'string' ? filterValues.name.trim() : ''
336
+ if (name) p.set('name', name)
282
337
  if (organizationId) p.set('organizationId', organizationId)
283
338
  if (normalizedRoleIds.length) {
284
339
  for (const id of normalizedRoleIds) p.append('roleId', id)
285
340
  }
286
341
  return p.toString()
287
- }, [page, search, organizationId, normalizedRoleIds])
342
+ }, [filterValues.name, page, search, organizationId, normalizedRoleIds])
288
343
 
289
344
  const { data: usersData, isLoading } = useQuery({
290
345
  queryKey: ['users', params, scopeVersion],
@@ -315,6 +370,7 @@ export default function UsersListPage() {
315
370
  const columns = React.useMemo<ColumnDef<Row>[]>(() => {
316
371
  const base: ColumnDef<Row>[] = [
317
372
  { accessorKey: 'email', header: 'Email' },
373
+ { accessorKey: 'name', header: t('auth.users.list.columns.name', 'Display name') },
318
374
  { accessorKey: 'organizationName', header: 'Organization' },
319
375
  { accessorKey: 'roles', header: 'Roles', cell: ({ row }) => (row.original.roles || []).join(', ') },
320
376
  ]
@@ -322,7 +378,7 @@ export default function UsersListPage() {
322
378
  base.splice(1, 0, { accessorKey: 'tenantName', header: 'Tenant' })
323
379
  }
324
380
  return base
325
- }, [showTenantColumn])
381
+ }, [showTenantColumn, t])
326
382
 
327
383
  const handleDelete = React.useCallback(async (row: Row) => {
328
384
  const confirmed = await confirm({
@@ -356,6 +412,7 @@ export default function UsersListPage() {
356
412
  )}
357
413
  columns={columns}
358
414
  data={rowsWithOrgNames}
415
+ exporter={exportConfig}
359
416
  searchValue={search}
360
417
  onSearchChange={handleSearchChange}
361
418
  filters={filters}
@@ -75,8 +75,18 @@ type UserSnapshots = {
75
75
 
76
76
  const passwordSchema = buildPasswordSchema()
77
77
 
78
+ const displayNameSchema = z.preprocess(
79
+ (value) => {
80
+ if (typeof value !== 'string') return value
81
+ const trimmed = value.trim()
82
+ return trimmed.length ? trimmed : undefined
83
+ },
84
+ z.string().trim().min(1).max(120).optional(),
85
+ )
86
+
78
87
  const createSchema = z.object({
79
88
  email: z.string().email(),
89
+ name: displayNameSchema,
80
90
  password: passwordSchema.optional(),
81
91
  sendInviteEmail: z.boolean().optional(),
82
92
  organizationId: z.string().uuid(),
@@ -89,6 +99,7 @@ const createSchema = z.object({
89
99
  const updateSchema = z.object({
90
100
  id: z.string().uuid(),
91
101
  email: z.string().email().optional(),
102
+ name: displayNameSchema,
92
103
  password: passwordSchema.optional(),
93
104
  organizationId: z.string().uuid().optional(),
94
105
  roles: z.array(z.string()).optional(),
@@ -196,6 +207,7 @@ const createUserCommand: CommandHandler<Record<string, unknown>, CreateUserResul
196
207
  entity: User,
197
208
  data: {
198
209
  email: parsed.email,
210
+ name: parsed.name,
199
211
  emailHash,
200
212
  passwordHash,
201
213
  isConfirmed: true,
@@ -451,6 +463,9 @@ const updateUserCommand: CommandHandler<Record<string, unknown>, User> = {
451
463
  entity.email = parsed.email
452
464
  entity.emailHash = emailHash
453
465
  }
466
+ if (parsed.name !== undefined) {
467
+ entity.name = parsed.name
468
+ }
454
469
  if (parsed.organizationId !== undefined) {
455
470
  entity.organizationId = parsed.organizationId
456
471
  entity.tenantId = tenantId ?? null
@@ -103,6 +103,14 @@ export const sidebarPreferencesInputSchema = z.object({
103
103
  // Optional helpers for CLI or admin forms
104
104
  export const userCreateSchema = z.object({
105
105
  email: z.string().email(),
106
+ name: z.preprocess(
107
+ (value) => {
108
+ if (typeof value !== 'string') return value
109
+ const trimmed = value.trim()
110
+ return trimmed.length ? trimmed : undefined
111
+ },
112
+ z.string().trim().min(1).max(120).optional(),
113
+ ),
106
114
  password: passwordSchema.optional(),
107
115
  sendInviteEmail: z.boolean().optional(),
108
116
  tenantId: z.string().uuid().optional(),
@@ -172,6 +172,7 @@
172
172
  "auth.users.form.errors.notFound": "Benutzer nicht gefunden",
173
173
  "auth.users.form.errors.widgetsAssign": "Dashboard-Widgets konnten dem neuen Benutzer nicht zugewiesen werden",
174
174
  "auth.users.form.field.email": "E-Mail",
175
+ "auth.users.form.field.name": "Anzeigename",
175
176
  "auth.users.form.field.newPassword": "Neues Passwort",
176
177
  "auth.users.form.field.organization": "Organisation",
177
178
  "auth.users.form.field.password": "Passwort",
@@ -190,8 +191,11 @@
190
191
  "auth.users.form.loading": "Benutzerdaten werden geladen...",
191
192
  "auth.users.form.title.create": "Benutzer erstellen",
192
193
  "auth.users.form.title.edit": "Benutzer bearbeiten",
194
+ "auth.users.list.columns.name": "Anzeigename",
193
195
  "auth.users.list.error.delete": "Benutzer konnte nicht gelöscht werden",
194
196
  "auth.users.list.error.load": "Benutzer konnten nicht geladen werden",
197
+ "auth.users.list.filters.name": "Anzeigename",
198
+ "auth.users.list.filters.namePlaceholder": "Nach Anzeigename filtern",
195
199
  "auth.users.widgets.errors.load": "Dashboard-Widgets können nicht geladen werden. Sie können sie später auf der Benutzerseite konfigurieren.",
196
200
  "auth.users.widgets.loading": "Widgets werden geladen…",
197
201
  "auth.users.widgets.mode.hint": "Neue Benutzer erben Widgets von ihren zugewiesenen Rollen. Überschreiben Sie die Auswahl, um eine eigene Zusammenstellung festzulegen.",