@open-mercato/core 0.6.8-develop.7101.1.bd9356828a → 0.7.1-develop.7102.1.b41f7e3e51

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 (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/login.js +19 -0
  3. package/dist/modules/auth/api/login.js.map +2 -2
  4. package/dist/modules/auth/api/routeError.js +11 -0
  5. package/dist/modules/auth/api/routeError.js.map +7 -0
  6. package/dist/modules/auth/api/users/acl/route.js +23 -7
  7. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  8. package/dist/modules/business_rules/backend/rules/[id]/page.meta.js +6 -1
  9. package/dist/modules/business_rules/backend/rules/[id]/page.meta.js.map +2 -2
  10. package/dist/modules/business_rules/backend/rules/create/page.meta.js +5 -1
  11. package/dist/modules/business_rules/backend/rules/create/page.meta.js.map +2 -2
  12. package/dist/modules/business_rules/backend/sets/[id]/page.meta.js +6 -1
  13. package/dist/modules/business_rules/backend/sets/[id]/page.meta.js.map +2 -2
  14. package/dist/modules/business_rules/backend/sets/create/page.meta.js +5 -1
  15. package/dist/modules/business_rules/backend/sets/create/page.meta.js.map +2 -2
  16. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +84 -0
  17. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +7 -0
  18. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js +7 -81
  19. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js.map +2 -2
  20. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +525 -0
  21. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +7 -0
  22. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +731 -0
  23. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +7 -0
  24. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +7 -727
  25. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  26. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +7 -521
  27. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  28. package/dist/modules/customer_accounts/extension-points.js +1 -1
  29. package/dist/modules/customer_accounts/extension-points.js.map +2 -2
  30. package/dist/modules/customer_accounts/lib/portalUrl.js +69 -0
  31. package/dist/modules/customer_accounts/lib/portalUrl.js.map +7 -0
  32. package/dist/modules/customers/backend/customers/companies/page.js +2 -2
  33. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  34. package/dist/modules/customers/backend/customers/deals/page.js +2 -2
  35. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  36. package/dist/modules/customers/backend/customers/people/page.js +2 -2
  37. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  38. package/dist/modules/customers/components/detail/AssignRoleDialog.js +33 -28
  39. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  40. package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js +3 -1
  41. package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js.map +2 -2
  42. package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js +1 -0
  43. package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js.map +2 -2
  44. package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js +4 -1
  45. package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js.map +2 -2
  46. package/dist/modules/eudr/backend/eudr/page.meta.js +2 -2
  47. package/dist/modules/eudr/backend/eudr/page.meta.js.map +2 -2
  48. package/dist/modules/eudr/backend/eudr/plots/[id]/page.js +8 -2
  49. package/dist/modules/eudr/backend/eudr/plots/[id]/page.js.map +2 -2
  50. package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js +3 -1
  51. package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js.map +2 -2
  52. package/dist/modules/eudr/backend/eudr/plots/create/page.js +8 -2
  53. package/dist/modules/eudr/backend/eudr/plots/create/page.js.map +2 -2
  54. package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js +1 -0
  55. package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js.map +2 -2
  56. package/dist/modules/eudr/backend/eudr/plots/page.meta.js +4 -1
  57. package/dist/modules/eudr/backend/eudr/plots/page.meta.js.map +2 -2
  58. package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js +3 -1
  59. package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js.map +2 -2
  60. package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js +1 -0
  61. package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js.map +2 -2
  62. package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js +4 -1
  63. package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js.map +2 -2
  64. package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js +3 -1
  65. package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js.map +2 -2
  66. package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js +1 -0
  67. package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js.map +2 -2
  68. package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js +4 -1
  69. package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js.map +2 -2
  70. package/dist/modules/eudr/backend/eudr/statements/[id]/page.js +2 -0
  71. package/dist/modules/eudr/backend/eudr/statements/[id]/page.js.map +2 -2
  72. package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js +3 -1
  73. package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js.map +2 -2
  74. package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js +1 -0
  75. package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js.map +2 -2
  76. package/dist/modules/eudr/backend/eudr/statements/page.js +30 -25
  77. package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
  78. package/dist/modules/eudr/backend/eudr/statements/page.meta.js +4 -1
  79. package/dist/modules/eudr/backend/eudr/statements/page.meta.js.map +2 -2
  80. package/dist/modules/eudr/commands/statements.js +5 -3
  81. package/dist/modules/eudr/commands/statements.js.map +2 -2
  82. package/dist/modules/eudr/components/crudErrorI18n.js +21 -1
  83. package/dist/modules/eudr/components/crudErrorI18n.js.map +2 -2
  84. package/dist/modules/eudr/components/formConfig.js +6 -2
  85. package/dist/modules/eudr/components/formConfig.js.map +2 -2
  86. package/dist/modules/eudr/lib/statement-lifecycle.js +9 -1
  87. package/dist/modules/eudr/lib/statement-lifecycle.js.map +2 -2
  88. package/dist/modules/sales/components/documents/LineItemDialog.js +1 -1
  89. package/dist/modules/sales/components/documents/LineItemDialog.js.map +1 -1
  90. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +44 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  92. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +20 -16
  93. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  94. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +10 -6
  95. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  96. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +11 -7
  97. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +2 -2
  98. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +10 -6
  99. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  100. package/package.json +8 -8
  101. package/src/modules/auth/api/login.ts +23 -0
  102. package/src/modules/auth/api/routeError.ts +30 -0
  103. package/src/modules/auth/api/users/acl/route.ts +49 -9
  104. package/src/modules/business_rules/backend/rules/[id]/page.meta.ts +5 -0
  105. package/src/modules/business_rules/backend/rules/create/page.meta.ts +5 -1
  106. package/src/modules/business_rules/backend/sets/[id]/page.meta.ts +5 -0
  107. package/src/modules/business_rules/backend/sets/create/page.meta.ts +5 -1
  108. package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +138 -0
  109. package/src/modules/customer_accounts/backend/customer_accounts/settings/page.tsx +8 -132
  110. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +601 -0
  111. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +838 -0
  112. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +6 -838
  113. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -590
  114. package/src/modules/customer_accounts/extension-points.ts +1 -1
  115. package/src/modules/customer_accounts/lib/portalUrl.ts +112 -0
  116. package/src/modules/customers/backend/customers/companies/page.tsx +2 -2
  117. package/src/modules/customers/backend/customers/deals/page.tsx +2 -2
  118. package/src/modules/customers/backend/customers/people/page.tsx +2 -2
  119. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -7
  120. package/src/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.ts +2 -0
  121. package/src/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.ts +1 -0
  122. package/src/modules/eudr/backend/eudr/evidence-submissions/page.meta.ts +4 -1
  123. package/src/modules/eudr/backend/eudr/page.meta.ts +2 -2
  124. package/src/modules/eudr/backend/eudr/plots/[id]/page.meta.ts +2 -0
  125. package/src/modules/eudr/backend/eudr/plots/[id]/page.tsx +8 -2
  126. package/src/modules/eudr/backend/eudr/plots/create/page.meta.ts +1 -0
  127. package/src/modules/eudr/backend/eudr/plots/create/page.tsx +8 -2
  128. package/src/modules/eudr/backend/eudr/plots/page.meta.ts +4 -1
  129. package/src/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.ts +2 -0
  130. package/src/modules/eudr/backend/eudr/product-mappings/create/page.meta.ts +1 -0
  131. package/src/modules/eudr/backend/eudr/product-mappings/page.meta.ts +4 -1
  132. package/src/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.ts +2 -0
  133. package/src/modules/eudr/backend/eudr/risk-assessments/create/page.meta.ts +1 -0
  134. package/src/modules/eudr/backend/eudr/risk-assessments/page.meta.ts +4 -1
  135. package/src/modules/eudr/backend/eudr/statements/[id]/page.meta.ts +2 -0
  136. package/src/modules/eudr/backend/eudr/statements/[id]/page.tsx +2 -0
  137. package/src/modules/eudr/backend/eudr/statements/create/page.meta.ts +1 -0
  138. package/src/modules/eudr/backend/eudr/statements/page.meta.ts +4 -1
  139. package/src/modules/eudr/backend/eudr/statements/page.tsx +32 -26
  140. package/src/modules/eudr/commands/statements.ts +4 -2
  141. package/src/modules/eudr/components/crudErrorI18n.ts +31 -0
  142. package/src/modules/eudr/components/formConfig.tsx +9 -1
  143. package/src/modules/eudr/i18n/de.json +3 -2
  144. package/src/modules/eudr/i18n/en.json +3 -2
  145. package/src/modules/eudr/i18n/es.json +3 -2
  146. package/src/modules/eudr/i18n/ko.json +3 -2
  147. package/src/modules/eudr/i18n/pl.json +3 -2
  148. package/src/modules/eudr/lib/statement-lifecycle.ts +8 -0
  149. package/src/modules/sales/components/documents/LineItemDialog.tsx +1 -1
  150. package/src/modules/sales/i18n/de.json +37 -0
  151. package/src/modules/sales/i18n/en.json +37 -0
  152. package/src/modules/sales/i18n/es.json +37 -0
  153. package/src/modules/sales/i18n/ko.json +37 -0
  154. package/src/modules/sales/i18n/pl.json +37 -0
  155. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +42 -2
  156. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +30 -26
  157. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +21 -17
  158. package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +17 -13
  159. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +16 -12
@@ -1,599 +1,15 @@
1
- "use client"
2
-
3
- import * as React from 'react'
4
- import { extensionPoints } from '@open-mercato/core/modules/customer_accounts/extension-points'
5
- import Link from 'next/link'
6
- import { useRouter } from 'next/navigation'
7
- import { Globe, Settings } from 'lucide-react'
1
+ import { headers } from 'next/headers'
8
2
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
9
- import { DataTable } from '@open-mercato/ui/backend/DataTable'
10
- import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
11
- import { RowActions } from '@open-mercato/ui/backend/RowActions'
12
- import { Button } from '@open-mercato/ui/primitives/button'
13
- import { Input } from '@open-mercato/ui/primitives/input'
14
- import { EmailInput } from '@open-mercato/ui/primitives/email-input'
15
- import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
16
- import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
17
- import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
18
- import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
19
- import { flash } from '@open-mercato/ui/backend/FlashMessages'
20
- import { useT } from '@open-mercato/shared/lib/i18n/context'
21
- import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
22
- import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
23
- import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
24
- import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
25
-
26
- type UserRow = {
27
- id: string
28
- displayName: string
29
- email: string
30
- emailVerified: boolean
31
- isActive: boolean
32
- lastLoginAt: string | null
33
- roles: Array<{ id: string; name: string; slug: string }>
34
- createdAt: string
35
- updatedAt?: string | null
36
- personEntityId: string | null
37
- customerEntityId: string | null
38
- }
39
-
40
- type UsersResponse = {
41
- items?: UserRow[]
42
- total?: number
43
- totalPages?: number
44
- totalIsCapped?: boolean
45
- }
46
-
47
- function formatDate(value: string | null | undefined, fallback: string): string {
48
- if (!value) return fallback
49
- const date = new Date(value)
50
- if (Number.isNaN(date.getTime())) return fallback
51
- return date.toLocaleDateString()
52
- }
53
-
54
- async function fetchRoleFilterOptions(): Promise<Array<{ value: string; label: string; id: string }>> {
55
- try {
56
- const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(
57
- '/api/customer_accounts/admin/roles?pageSize=100',
58
- )
59
- if (!call.ok) return []
60
- const items = Array.isArray(call.result?.items) ? call.result!.items : []
61
- return items
62
- .filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string')
63
- .map((item) => ({ value: item.id, label: item.name, id: item.id }))
64
- } catch {
65
- return []
66
- }
67
- }
68
-
69
- function CreateUserDialog({
70
- open,
71
- onOpenChange,
72
- roleOptions,
73
- onCreated,
74
- onRunMutation,
75
- }: {
76
- open: boolean
77
- onOpenChange: (next: boolean) => void
78
- roleOptions: Array<{ id: string; label: string }>
79
- onCreated: () => void
80
- onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>
81
- }) {
82
- const t = useT()
83
- const [email, setEmail] = React.useState('')
84
- const [displayName, setDisplayName] = React.useState('')
85
- const [password, setPassword] = React.useState('')
86
- const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])
87
- const [isSubmitting, setIsSubmitting] = React.useState(false)
88
-
89
- const resetForm = React.useCallback(() => {
90
- setEmail('')
91
- setDisplayName('')
92
- setPassword('')
93
- setSelectedRoleIds([])
94
- }, [])
95
-
96
- const handleSubmit = React.useCallback(async (event: React.FormEvent) => {
97
- event.preventDefault()
98
- if (!email.trim() || !displayName.trim() || !password.trim()) {
99
- flash(t('customer_accounts.admin.createUser.error.required', 'Email, name, and password are required'), 'error')
100
- return
101
- }
102
- setIsSubmitting(true)
103
- try {
104
- await onRunMutation(async () => {
105
- const call = await apiCall<{ ok: boolean; error?: string }>(
106
- '/api/customer_accounts/admin/users',
107
- {
108
- method: 'POST',
109
- headers: { 'content-type': 'application/json' },
110
- body: JSON.stringify({
111
- email: email.trim(),
112
- displayName: displayName.trim(),
113
- password,
114
- roleIds: selectedRoleIds.length > 0 ? selectedRoleIds : undefined,
115
- }),
116
- },
117
- )
118
- if (!call.ok) {
119
- flash(call.result?.error || t('customer_accounts.admin.createUser.error.save', 'Failed to create user'), 'error')
120
- return
121
- }
122
- flash(t('customer_accounts.admin.createUser.flash.created', 'User created'), 'success')
123
- resetForm()
124
- onOpenChange(false)
125
- onCreated()
126
- })
127
- } catch (err) {
128
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.createUser.error.save', 'Failed to create user')
129
- flash(message, 'error')
130
- } finally {
131
- setIsSubmitting(false)
132
- }
133
- }, [displayName, email, onCreated, onOpenChange, onRunMutation, password, resetForm, selectedRoleIds, t])
134
-
135
- const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
136
- if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
137
- event.preventDefault()
138
- const form = (event.target as HTMLElement).closest('form')
139
- if (form) form.requestSubmit()
140
- }
141
- }, [])
142
-
143
- return (
144
- <Dialog open={open} onOpenChange={(next) => { if (!next) resetForm(); onOpenChange(next) }}>
145
- <DialogContent className="sm:max-w-lg">
146
- <DialogHeader>
147
- <DialogTitle>{t('customer_accounts.admin.createUser.title', 'Create Customer User')}</DialogTitle>
148
- </DialogHeader>
149
- <form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className="space-y-4">
150
- <div className="space-y-2">
151
- <label className="text-sm font-medium" htmlFor="create-email">
152
- {t('customer_accounts.admin.createUser.fields.email', 'Email')}
153
- </label>
154
- <EmailInput
155
- id="create-email"
156
- required
157
- value={email}
158
- onChange={(event) => setEmail(event.target.value)}
159
- placeholder={t('customer_accounts.admin.createUser.fields.emailPlaceholder', 'user@example.com')}
160
- />
161
- </div>
162
- <div className="space-y-2">
163
- <label className="text-sm font-medium" htmlFor="create-name">
164
- {t('customer_accounts.admin.createUser.fields.displayName', 'Display Name')}
165
- </label>
166
- <Input
167
- id="create-name"
168
- type="text"
169
- required
170
- value={displayName}
171
- onChange={(event) => setDisplayName(event.target.value)}
172
- placeholder={t('customer_accounts.admin.createUser.fields.displayNamePlaceholder', 'John Doe')}
173
- />
174
- </div>
175
- <div className="space-y-2">
176
- <label className="text-sm font-medium" htmlFor="create-password">
177
- {t('customer_accounts.admin.createUser.fields.password', 'Password')}
178
- </label>
179
- <PasswordInput
180
- id="create-password"
181
- required
182
- minLength={8}
183
- value={password}
184
- onChange={(event) => setPassword(event.target.value)}
185
- placeholder={t('customer_accounts.admin.createUser.fields.passwordPlaceholder', 'Min. 8 characters')}
186
- autoComplete="new-password"
187
- />
188
- </div>
189
- {roleOptions.length > 0 && (
190
- <div className="space-y-2">
191
- <p className="text-sm font-medium">{t('customer_accounts.admin.createUser.fields.roles', 'Roles')}</p>
192
- <div className="flex flex-wrap gap-2">
193
- {roleOptions.map((role) => {
194
- const isSelected = selectedRoleIds.includes(role.id)
195
- return (
196
- <Button
197
- key={role.id}
198
- type="button"
199
- size="2xs"
200
- variant={isSelected ? 'secondary' : 'outline'}
201
- aria-pressed={isSelected}
202
- onClick={() => setSelectedRoleIds((prev) =>
203
- prev.includes(role.id) ? prev.filter((rid) => rid !== role.id) : [...prev, role.id],
204
- )}
205
- className="rounded-full"
206
- >
207
- {role.label}
208
- </Button>
209
- )
210
- })}
211
- </div>
212
- </div>
213
- )}
214
- <div className="flex justify-end gap-2 pt-2">
215
- <Button type="button" variant="outline" onClick={() => { resetForm(); onOpenChange(false) }}>
216
- {t('customer_accounts.admin.createUser.actions.cancel', 'Cancel')}
217
- </Button>
218
- <Button type="submit" disabled={isSubmitting}>
219
- {isSubmitting
220
- ? t('customer_accounts.admin.createUser.actions.creating', 'Creating...')
221
- : t('customer_accounts.admin.createUser.actions.create', 'Create User')}
222
- </Button>
223
- </div>
224
- </form>
225
- </DialogContent>
226
- </Dialog>
227
- )
228
- }
229
-
230
- export default function CustomerAccountsPage() {
231
- const { confirm, ConfirmDialogElement } = useConfirmDialog()
232
- const t = useT()
233
- const router = useRouter()
234
- const [rows, setRows] = React.useState<UserRow[]>([])
235
- const [page, setPage] = React.useState(1)
236
- const [pageSize] = React.useState(50)
237
- const [total, setTotal] = React.useState(0)
238
- const [totalPages, setTotalPages] = React.useState(1)
239
- const [totalIsCapped, setTotalIsCapped] = React.useState(false)
240
- const [search, setSearch] = React.useState('')
241
- const [filterValues, setFilterValues] = React.useState<FilterValues>({})
242
- const [isLoading, setIsLoading] = React.useState(true)
243
- const [reloadToken, setReloadToken] = React.useState(0)
244
- const [roleOptions, setRoleOptions] = React.useState<Array<{ value: string; label: string; id: string }>>([])
245
- const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
246
-
247
- const { runMutation, retryLastMutation } = useGuardedMutation<{
248
- entityType: string
249
- entityId?: string
250
- }>({
251
- contextId: 'customer_accounts:users-list',
252
- })
253
-
254
- const runMutationWithContext = React.useCallback(
255
- async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
256
- return runMutation({
257
- operation,
258
- mutationPayload,
259
- context: { entityType: 'customer_accounts:user' },
260
- })
261
- },
262
- [runMutation],
263
- )
264
-
265
- React.useEffect(() => {
266
- let cancelled = false
267
- fetchRoleFilterOptions().then((opts) => {
268
- if (!cancelled) setRoleOptions(opts)
269
- })
270
- return () => { cancelled = true }
271
- }, [])
272
-
273
- const filters = React.useMemo<FilterDef[]>(() => [
274
- {
275
- id: 'status',
276
- label: t('customer_accounts.admin.filters.status', 'Status'),
277
- type: 'select',
278
- options: [
279
- { value: 'active', label: t('customer_accounts.admin.filters.active', 'Active') },
280
- { value: 'inactive', label: t('customer_accounts.admin.filters.inactive', 'Inactive') },
281
- ],
282
- },
283
- {
284
- id: 'roleId',
285
- label: t('customer_accounts.admin.filters.role', 'Role'),
286
- type: 'select',
287
- options: roleOptions,
288
- },
289
- ], [roleOptions, t])
290
-
291
- const queryParams = React.useMemo(() => {
292
- const params = new URLSearchParams()
293
- params.set('page', String(page))
294
- params.set('pageSize', String(pageSize))
295
- if (search.trim()) params.set('search', search.trim())
296
- const status = filterValues.status
297
- if (typeof status === 'string' && status.trim()) params.set('status', status)
298
- const roleId = filterValues.roleId
299
- if (typeof roleId === 'string' && roleId.trim()) params.set('roleId', roleId)
300
- return params.toString()
301
- }, [filterValues, page, pageSize, search])
302
-
303
- React.useEffect(() => {
304
- let cancelled = false
305
- async function load() {
306
- setIsLoading(true)
307
- try {
308
- const fallback: UsersResponse = { items: [], total: 0, totalPages: 1 }
309
- const payload = await readApiResultOrThrow<UsersResponse>(
310
- `/api/customer_accounts/admin/users?${queryParams}`,
311
- undefined,
312
- { errorMessage: t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users'), fallback },
313
- )
314
- if (cancelled) return
315
- const items = Array.isArray(payload?.items) ? payload.items : []
316
- setRows(items)
317
- setTotal(typeof payload?.total === 'number' ? payload.total : items.length)
318
- setTotalPages(typeof payload?.totalPages === 'number' ? payload.totalPages : 1)
319
- setTotalIsCapped(payload?.totalIsCapped === true)
320
- } catch (err) {
321
- if (!cancelled) {
322
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.loadUsers', 'Failed to load customer users')
323
- flash(message, 'error')
324
- }
325
- } finally {
326
- if (!cancelled) setIsLoading(false)
327
- }
328
- }
329
- load()
330
- return () => { cancelled = true }
331
- }, [queryParams, reloadToken, t])
332
-
333
- const handleToggleActive = React.useCallback(async (user: UserRow) => {
334
- const nextActive = !user.isActive
335
- const actionLabel = nextActive
336
- ? t('customer_accounts.admin.actions.activate', 'Activate')
337
- : t('customer_accounts.admin.actions.deactivate', 'Deactivate')
338
- const confirmed = await confirm({
339
- title: t('customer_accounts.admin.confirm.toggleActive', '{{action}} user "{{name}}"?', {
340
- action: actionLabel,
341
- name: user.displayName || user.email,
342
- }),
343
- variant: nextActive ? 'default' : 'destructive',
344
- })
345
- if (!confirmed) return
346
- try {
347
- await runMutationWithContext(async () => {
348
- const call = await withScopedApiRequestHeaders(
349
- buildOptimisticLockHeader(user.updatedAt),
350
- () => apiCall(
351
- `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,
352
- {
353
- method: 'PUT',
354
- headers: { 'content-type': 'application/json' },
355
- body: JSON.stringify({ isActive: nextActive }),
356
- },
357
- ),
358
- )
359
- if (!call.ok) {
360
- flash(t('customer_accounts.admin.error.toggleActive', 'Failed to update user status'), 'error')
361
- return
362
- }
363
- flash(
364
- nextActive
365
- ? t('customer_accounts.admin.flash.activated', 'User activated')
366
- : t('customer_accounts.admin.flash.deactivated', 'User deactivated'),
367
- 'success',
368
- )
369
- setReloadToken((token) => token + 1)
370
- }, { userId: user.id, isActive: nextActive })
371
- } catch (err) {
372
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.toggleActive', 'Failed to update user status')
373
- flash(message, 'error')
374
- }
375
- }, [confirm, runMutationWithContext, t])
376
-
377
- const handleDelete = React.useCallback(async (user: UserRow) => {
378
- const confirmed = await confirm({
379
- title: t('customer_accounts.admin.confirm.delete', 'Delete user "{{name}}"?', {
380
- name: user.displayName || user.email,
381
- }),
382
- variant: 'destructive',
383
- })
384
- if (!confirmed) return
385
- try {
386
- await runMutationWithContext(async () => {
387
- const call = await withScopedApiRequestHeaders(
388
- buildOptimisticLockHeader(user.updatedAt),
389
- () => apiCall(
390
- `/api/customer_accounts/admin/users/${encodeURIComponent(user.id)}`,
391
- { method: 'DELETE' },
392
- ),
393
- )
394
- if (!call.ok) {
395
- flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')
396
- return
397
- }
398
- flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')
399
- setReloadToken((token) => token + 1)
400
- }, { id: user.id })
401
- } catch (err) {
402
- const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')
403
- flash(message, 'error')
404
- }
405
- }, [confirm, runMutationWithContext, t])
406
-
407
- const handleFiltersApply = React.useCallback((values: FilterValues) => {
408
- setFilterValues(values)
409
- setPage(1)
410
- }, [])
411
-
412
- const handleFiltersClear = React.useCallback(() => {
413
- setFilterValues({})
414
- setPage(1)
415
- }, [])
416
-
417
- const columns = React.useMemo<ColumnDef<UserRow>[]>(() => {
418
- const noValue = <span className="text-muted-foreground text-sm">-</span>
419
- return [
420
- {
421
- accessorKey: 'displayName',
422
- header: t('customer_accounts.admin.columns.displayName', 'Name'),
423
- cell: ({ row }) => (
424
- <Link
425
- href={`/backend/customer_accounts/users/${row.original.id}`}
426
- className="font-medium hover:underline"
427
- >
428
- {row.original.displayName || row.original.email}
429
- </Link>
430
- ),
431
- },
432
- {
433
- accessorKey: 'email',
434
- header: t('customer_accounts.admin.columns.email', 'Email'),
435
- },
436
- {
437
- accessorKey: 'emailVerified',
438
- header: t('customer_accounts.admin.columns.emailVerified', 'Verified'),
439
- cell: ({ row }) => (
440
- <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
441
- row.original.emailVerified
442
- ? 'bg-status-success-bg text-status-success-text'
443
- : 'bg-status-warning-bg text-status-warning-text'
444
- }`}>
445
- {row.original.emailVerified
446
- ? t('customer_accounts.admin.verified', 'Yes')
447
- : t('customer_accounts.admin.unverified', 'No')}
448
- </span>
449
- ),
450
- },
451
- {
452
- accessorKey: 'isActive',
453
- header: t('customer_accounts.admin.columns.status', 'Status'),
454
- cell: ({ row }) => (
455
- <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
456
- row.original.isActive
457
- ? 'bg-status-success-bg text-status-success-text'
458
- : 'bg-status-error-bg text-status-error-text'
459
- }`}>
460
- {row.original.isActive
461
- ? t('customer_accounts.admin.active', 'Active')
462
- : t('customer_accounts.admin.inactive', 'Inactive')}
463
- </span>
464
- ),
465
- },
466
- {
467
- accessorKey: 'lastLoginAt',
468
- header: t('customer_accounts.admin.columns.lastLogin', 'Last Login'),
469
- cell: ({ row }) => formatDate(row.original.lastLoginAt, '-') || noValue,
470
- },
471
- {
472
- accessorKey: 'roles',
473
- header: t('customer_accounts.admin.columns.roles', 'Roles'),
474
- cell: ({ row }) => {
475
- const roles = row.original.roles
476
- if (!roles || !roles.length) return noValue
477
- return <span className="text-sm">{roles.map((r) => r.name).join(', ')}</span>
478
- },
479
- },
480
- {
481
- accessorKey: 'createdAt',
482
- header: t('customer_accounts.admin.columns.createdAt', 'Created'),
483
- cell: ({ row }) => formatDate(row.original.createdAt, '-'),
484
- },
485
- ]
486
- }, [t])
3
+ import { resolvePortalRequestOrigin } from '../../../lib/portalUrl'
4
+ import { PortalUsersPageClient } from './PortalUsersPageClient'
487
5
 
6
+ export default async function CustomerAccountsPage() {
7
+ const portalOrigin = resolvePortalRequestOrigin(await headers())
488
8
  return (
489
9
  <Page>
490
10
  <PageBody className="space-y-4">
491
- <div className="rounded-lg border border-status-info-border bg-status-info-bg p-4">
492
- <div className="flex items-start justify-between gap-4">
493
- <div>
494
- <h3 className="text-sm font-medium text-status-info-text">
495
- {t('customer_accounts.admin.portalInfo.title', 'Customer Portal')}
496
- </h3>
497
- <p className="mt-1 text-sm text-status-info-text">
498
- {t('customer_accounts.admin.portalInfo.description', 'Manage customer portal accounts. Customers can self-register, log in, and access orders, quotes, and invoices through the portal.')}
499
- </p>
500
- <p className="mt-1.5 text-xs text-status-info-text">
501
- {t('customer_accounts.admin.portalInfo.url', 'Portal URL: {url}', {
502
- url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,
503
- })}
504
- </p>
505
- <p className="mt-0.5 text-xs text-status-info-text">
506
- {t('customer_accounts.admin.portalInfo.credentials', 'Demo credentials: alice.johnson@example.com / Password123!')}
507
- </p>
508
- </div>
509
- <div className="flex shrink-0 flex-col gap-2">
510
- <Button
511
- type="button"
512
- variant="outline"
513
- size="sm"
514
- asChild
515
- >
516
- <Link href="/backend/customer_accounts/settings">
517
- <Settings className="size-4" />
518
- {t('customer_accounts.admin.portalInfo.openConfiguration', 'Open Configuration')}
519
- </Link>
520
- </Button>
521
- <Button
522
- type="button"
523
- variant="outline"
524
- size="sm"
525
- asChild
526
- >
527
- <a href={`${typeof window !== 'undefined' ? window.location.origin : ''}/portal`} target="_blank" rel="noopener noreferrer">
528
- <Globe className="size-4" />
529
- {t('customer_accounts.admin.portalInfo.open', 'Open Portal')}
530
- </a>
531
- </Button>
532
- </div>
533
- </div>
534
- </div>
535
- <DataTable<UserRow>
536
- stickyActionsColumn
537
- title={t('customer_accounts.admin.title', 'Users')}
538
- actions={(
539
- <Button onClick={() => setCreateDialogOpen(true)}>
540
- {t('customer_accounts.admin.actions.createUser', 'Create User')}
541
- </Button>
542
- )}
543
- columns={columns}
544
- data={rows}
545
- searchValue={search}
546
- onSearchChange={(value) => { setSearch(value); setPage(1) }}
547
- searchPlaceholder={t('customer_accounts.admin.searchPlaceholder', 'Search by name or email...')}
548
- filters={filters}
549
- filterValues={filterValues}
550
- onFiltersApply={handleFiltersApply}
551
- onFiltersClear={handleFiltersClear}
552
- perspective={{ tableId: extensionPoints.hosts.usersTable.tableId }}
553
- emptyState={(
554
- <ListEmptyState
555
- entityName={t('customer_accounts.admin.title', 'Users')}
556
- onCreate={() => setCreateDialogOpen(true)}
557
- createLabel={t('customer_accounts.admin.actions.createUser', 'Create User')}
558
- />
559
- )}
560
- onRowClick={(row) => router.push(`/backend/customer_accounts/users/${row.id}`)}
561
- rowActions={(row) => (
562
- <RowActions
563
- items={[
564
- {
565
- id: 'view',
566
- label: t('customer_accounts.admin.actions.view', 'View'),
567
- onSelect: () => { router.push(`/backend/customer_accounts/users/${row.id}`) },
568
- },
569
- {
570
- id: 'toggle-active',
571
- label: row.isActive
572
- ? t('customer_accounts.admin.actions.deactivate', 'Deactivate')
573
- : t('customer_accounts.admin.actions.activate', 'Activate'),
574
- onSelect: () => { void handleToggleActive(row) },
575
- },
576
- {
577
- id: 'delete',
578
- label: t('customer_accounts.admin.actions.delete', 'Delete'),
579
- destructive: true,
580
- onSelect: () => { void handleDelete(row) },
581
- },
582
- ]}
583
- />
584
- )}
585
- pagination={{ page, pageSize, total, totalPages, totalIsCapped, onPageChange: setPage }}
586
- isLoading={isLoading}
587
- />
588
- <CreateUserDialog
589
- open={createDialogOpen}
590
- onOpenChange={setCreateDialogOpen}
591
- roleOptions={roleOptions}
592
- onCreated={() => setReloadToken((token) => token + 1)}
593
- onRunMutation={runMutationWithContext}
594
- />
11
+ <PortalUsersPageClient portalOrigin={portalOrigin} />
595
12
  </PageBody>
596
- {ConfirmDialogElement}
597
13
  </Page>
598
14
  )
599
15
  }
@@ -3,7 +3,7 @@ import { dataTableExtensionHost, defineModuleExtensionPoints } from '@open-merca
3
3
  export const extensionPoints = defineModuleExtensionPoints({
4
4
  moduleId: 'customer_accounts',
5
5
  hosts: {
6
- usersTable: dataTableExtensionHost({ tableId: 'customer_accounts.admin.users', source: 'backend/customer_accounts/users/page.tsx' }),
6
+ usersTable: dataTableExtensionHost({ tableId: 'customer_accounts.admin.users', source: 'backend/customer_accounts/users/PortalUsersPageClient.tsx' }),
7
7
  rolesTable: dataTableExtensionHost({ tableId: 'customer_accounts.admin.roles', source: 'backend/customer_accounts/roles/page.tsx' }),
8
8
  },
9
9
  })