@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/auth/api/login.js +19 -0
- package/dist/modules/auth/api/login.js.map +2 -2
- package/dist/modules/auth/api/routeError.js +11 -0
- package/dist/modules/auth/api/routeError.js.map +7 -0
- package/dist/modules/auth/api/users/acl/route.js +23 -7
- package/dist/modules/auth/api/users/acl/route.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/[id]/page.meta.js +6 -1
- package/dist/modules/business_rules/backend/rules/[id]/page.meta.js.map +2 -2
- package/dist/modules/business_rules/backend/rules/create/page.meta.js +5 -1
- package/dist/modules/business_rules/backend/rules/create/page.meta.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/[id]/page.meta.js +6 -1
- package/dist/modules/business_rules/backend/sets/[id]/page.meta.js.map +2 -2
- package/dist/modules/business_rules/backend/sets/create/page.meta.js +5 -1
- package/dist/modules/business_rules/backend/sets/create/page.meta.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +84 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js +7 -81
- package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +525 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +731 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +7 -727
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +7 -521
- package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
- package/dist/modules/customer_accounts/extension-points.js +1 -1
- package/dist/modules/customer_accounts/extension-points.js.map +2 -2
- package/dist/modules/customer_accounts/lib/portalUrl.js +69 -0
- package/dist/modules/customer_accounts/lib/portalUrl.js.map +7 -0
- package/dist/modules/customers/backend/customers/companies/page.js +2 -2
- package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people/page.js +2 -2
- package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +33 -28
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js +3 -1
- package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js +1 -0
- package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js +4 -1
- package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/page.meta.js +2 -2
- package/dist/modules/eudr/backend/eudr/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.js +8 -2
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js +3 -1
- package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/create/page.js +8 -2
- package/dist/modules/eudr/backend/eudr/plots/create/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js +1 -0
- package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/plots/page.meta.js +4 -1
- package/dist/modules/eudr/backend/eudr/plots/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js +3 -1
- package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js +1 -0
- package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js +4 -1
- package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js +3 -1
- package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js +1 -0
- package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js +4 -1
- package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.js +2 -0
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js +3 -1
- package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js +1 -0
- package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.js +30 -25
- package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
- package/dist/modules/eudr/backend/eudr/statements/page.meta.js +4 -1
- package/dist/modules/eudr/backend/eudr/statements/page.meta.js.map +2 -2
- package/dist/modules/eudr/commands/statements.js +5 -3
- package/dist/modules/eudr/commands/statements.js.map +2 -2
- package/dist/modules/eudr/components/crudErrorI18n.js +21 -1
- package/dist/modules/eudr/components/crudErrorI18n.js.map +2 -2
- package/dist/modules/eudr/components/formConfig.js +6 -2
- package/dist/modules/eudr/components/formConfig.js.map +2 -2
- package/dist/modules/eudr/lib/statement-lifecycle.js +9 -1
- package/dist/modules/eudr/lib/statement-lifecycle.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +1 -1
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +1 -1
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js +44 -2
- package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +20 -16
- package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js +10 -6
- package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +11 -7
- package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +2 -2
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +10 -6
- package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/auth/api/login.ts +23 -0
- package/src/modules/auth/api/routeError.ts +30 -0
- package/src/modules/auth/api/users/acl/route.ts +49 -9
- package/src/modules/business_rules/backend/rules/[id]/page.meta.ts +5 -0
- package/src/modules/business_rules/backend/rules/create/page.meta.ts +5 -1
- package/src/modules/business_rules/backend/sets/[id]/page.meta.ts +5 -0
- package/src/modules/business_rules/backend/sets/create/page.meta.ts +5 -1
- package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +138 -0
- package/src/modules/customer_accounts/backend/customer_accounts/settings/page.tsx +8 -132
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +601 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +838 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +6 -838
- package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -590
- package/src/modules/customer_accounts/extension-points.ts +1 -1
- package/src/modules/customer_accounts/lib/portalUrl.ts +112 -0
- package/src/modules/customers/backend/customers/companies/page.tsx +2 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +2 -2
- package/src/modules/customers/backend/customers/people/page.tsx +2 -2
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -7
- package/src/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.ts +2 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.ts +1 -0
- package/src/modules/eudr/backend/eudr/evidence-submissions/page.meta.ts +4 -1
- package/src/modules/eudr/backend/eudr/page.meta.ts +2 -2
- package/src/modules/eudr/backend/eudr/plots/[id]/page.meta.ts +2 -0
- package/src/modules/eudr/backend/eudr/plots/[id]/page.tsx +8 -2
- package/src/modules/eudr/backend/eudr/plots/create/page.meta.ts +1 -0
- package/src/modules/eudr/backend/eudr/plots/create/page.tsx +8 -2
- package/src/modules/eudr/backend/eudr/plots/page.meta.ts +4 -1
- package/src/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.ts +2 -0
- package/src/modules/eudr/backend/eudr/product-mappings/create/page.meta.ts +1 -0
- package/src/modules/eudr/backend/eudr/product-mappings/page.meta.ts +4 -1
- package/src/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.ts +2 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/create/page.meta.ts +1 -0
- package/src/modules/eudr/backend/eudr/risk-assessments/page.meta.ts +4 -1
- package/src/modules/eudr/backend/eudr/statements/[id]/page.meta.ts +2 -0
- package/src/modules/eudr/backend/eudr/statements/[id]/page.tsx +2 -0
- package/src/modules/eudr/backend/eudr/statements/create/page.meta.ts +1 -0
- package/src/modules/eudr/backend/eudr/statements/page.meta.ts +4 -1
- package/src/modules/eudr/backend/eudr/statements/page.tsx +32 -26
- package/src/modules/eudr/commands/statements.ts +4 -2
- package/src/modules/eudr/components/crudErrorI18n.ts +31 -0
- package/src/modules/eudr/components/formConfig.tsx +9 -1
- package/src/modules/eudr/i18n/de.json +3 -2
- package/src/modules/eudr/i18n/en.json +3 -2
- package/src/modules/eudr/i18n/es.json +3 -2
- package/src/modules/eudr/i18n/ko.json +3 -2
- package/src/modules/eudr/i18n/pl.json +3 -2
- package/src/modules/eudr/lib/statement-lifecycle.ts +8 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +1 -1
- package/src/modules/sales/i18n/de.json +37 -0
- package/src/modules/sales/i18n/en.json +37 -0
- package/src/modules/sales/i18n/es.json +37 -0
- package/src/modules/sales/i18n/ko.json +37 -0
- package/src/modules/sales/i18n/pl.json +37 -0
- package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +42 -2
- package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +30 -26
- package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +21 -17
- package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +17 -13
- package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +16 -12
|
@@ -1,847 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as React from 'react'
|
|
4
|
-
import Link from 'next/link'
|
|
5
|
-
import { useRouter } from 'next/navigation'
|
|
1
|
+
import { headers } from 'next/headers'
|
|
6
2
|
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
|
-
import { PasswordInput } from '@open-mercato/ui/primitives/password-input'
|
|
11
|
-
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
12
|
-
import { SwitchField } from '@open-mercato/ui/primitives/switch-field'
|
|
13
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
|
|
14
|
-
import { apiCall, readApiResultOrThrow, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
15
|
-
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
16
|
-
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
17
|
-
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
18
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
19
|
-
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
20
|
-
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
21
|
-
import { RecordNotFoundState, ErrorMessage } from '@open-mercato/ui/backend/detail'
|
|
22
|
-
|
|
23
|
-
type UserDetail = {
|
|
24
|
-
id: string
|
|
25
|
-
displayName: string
|
|
26
|
-
email: string
|
|
27
|
-
emailVerifiedAt: string | null
|
|
28
|
-
isActive: boolean
|
|
29
|
-
lastLoginAt: string | null
|
|
30
|
-
personEntityId: string | null
|
|
31
|
-
customerEntityId: string | null
|
|
32
|
-
createdAt: string
|
|
33
|
-
updatedAt: string | null
|
|
34
|
-
roles: Array<{ id: string; name: string; slug: string }>
|
|
35
|
-
sessions: Array<{
|
|
36
|
-
id: string
|
|
37
|
-
ipAddress: string | null
|
|
38
|
-
userAgent: string | null
|
|
39
|
-
lastUsedAt: string | null
|
|
40
|
-
createdAt: string
|
|
41
|
-
expiresAt: string
|
|
42
|
-
}>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function formatDate(value: string | null | undefined, fallback: string): string {
|
|
46
|
-
if (!value) return fallback
|
|
47
|
-
const date = new Date(value)
|
|
48
|
-
if (Number.isNaN(date.getTime())) return fallback
|
|
49
|
-
return date.toLocaleString()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function ResetPasswordDialog({
|
|
53
|
-
open,
|
|
54
|
-
onOpenChange,
|
|
55
|
-
userId,
|
|
56
|
-
onRunMutation,
|
|
57
|
-
}: {
|
|
58
|
-
open: boolean
|
|
59
|
-
onOpenChange: (next: boolean) => void
|
|
60
|
-
userId: string
|
|
61
|
-
onRunMutation: <T>(operation: () => Promise<T>) => Promise<T>
|
|
62
|
-
}) {
|
|
63
|
-
const t = useT()
|
|
64
|
-
const [newPassword, setNewPassword] = React.useState('')
|
|
65
|
-
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
66
|
-
|
|
67
|
-
const handleSubmit = React.useCallback(async (event: React.FormEvent) => {
|
|
68
|
-
event.preventDefault()
|
|
69
|
-
if (!newPassword.trim() || newPassword.length < 8) {
|
|
70
|
-
flash(t('customer_accounts.admin.detail.resetPassword.error.minLength', 'Password must be at least 8 characters'), 'error')
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
setIsSubmitting(true)
|
|
74
|
-
try {
|
|
75
|
-
await onRunMutation(async () => {
|
|
76
|
-
const call = await apiCall<{ ok: boolean; error?: string }>(
|
|
77
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(userId)}/reset-password`,
|
|
78
|
-
{
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers: { 'content-type': 'application/json' },
|
|
81
|
-
body: JSON.stringify({ newPassword }),
|
|
82
|
-
},
|
|
83
|
-
)
|
|
84
|
-
if (!call.ok) {
|
|
85
|
-
flash(call.result?.error || t('customer_accounts.admin.detail.resetPassword.error.save', 'Failed to reset password'), 'error')
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
flash(t('customer_accounts.admin.detail.resetPassword.flash.success', 'Password reset successfully'), 'success')
|
|
89
|
-
setNewPassword('')
|
|
90
|
-
onOpenChange(false)
|
|
91
|
-
})
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.resetPassword.error.save', 'Failed to reset password')
|
|
94
|
-
flash(message, 'error')
|
|
95
|
-
} finally {
|
|
96
|
-
setIsSubmitting(false)
|
|
97
|
-
}
|
|
98
|
-
}, [newPassword, onOpenChange, onRunMutation, t, userId])
|
|
99
|
-
|
|
100
|
-
const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => {
|
|
101
|
-
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
102
|
-
event.preventDefault()
|
|
103
|
-
const form = (event.target as HTMLElement).closest('form')
|
|
104
|
-
if (form) form.requestSubmit()
|
|
105
|
-
}
|
|
106
|
-
}, [])
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<Dialog open={open} onOpenChange={(next) => { if (!next) setNewPassword(''); onOpenChange(next) }}>
|
|
110
|
-
<DialogContent className="sm:max-w-md">
|
|
111
|
-
<DialogHeader>
|
|
112
|
-
<DialogTitle>{t('customer_accounts.admin.detail.resetPassword.title', 'Reset Password')}</DialogTitle>
|
|
113
|
-
</DialogHeader>
|
|
114
|
-
<form onSubmit={(event) => { void handleSubmit(event) }} onKeyDown={handleKeyDown} className="space-y-4">
|
|
115
|
-
<div className="space-y-2">
|
|
116
|
-
<label className="text-sm font-medium" htmlFor="reset-password">
|
|
117
|
-
{t('customer_accounts.admin.detail.resetPassword.fields.newPassword', 'New Password')}
|
|
118
|
-
</label>
|
|
119
|
-
<PasswordInput
|
|
120
|
-
id="reset-password"
|
|
121
|
-
required
|
|
122
|
-
minLength={8}
|
|
123
|
-
value={newPassword}
|
|
124
|
-
onChange={(event) => setNewPassword(event.target.value)}
|
|
125
|
-
placeholder={t('customer_accounts.admin.detail.resetPassword.fields.placeholder', 'Min. 8 characters')}
|
|
126
|
-
autoComplete="new-password"
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
129
|
-
<div className="flex justify-end gap-2 pt-2">
|
|
130
|
-
<Button type="button" variant="outline" onClick={() => { setNewPassword(''); onOpenChange(false) }}>
|
|
131
|
-
{t('customer_accounts.admin.detail.resetPassword.actions.cancel', 'Cancel')}
|
|
132
|
-
</Button>
|
|
133
|
-
<Button type="submit" disabled={isSubmitting}>
|
|
134
|
-
{isSubmitting
|
|
135
|
-
? t('customer_accounts.admin.detail.resetPassword.actions.resetting', 'Resetting...')
|
|
136
|
-
: t('customer_accounts.admin.detail.resetPassword.actions.reset', 'Reset Password')}
|
|
137
|
-
</Button>
|
|
138
|
-
</div>
|
|
139
|
-
</form>
|
|
140
|
-
</DialogContent>
|
|
141
|
-
</Dialog>
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export default function CustomerUserDetailPage({ params }: { params?: { id?: string } }) {
|
|
146
|
-
const id = params?.id
|
|
147
|
-
const t = useT()
|
|
148
|
-
const router = useRouter()
|
|
149
|
-
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
150
|
-
const [data, setData] = React.useState<UserDetail | null>(null)
|
|
151
|
-
const [isLoading, setIsLoading] = React.useState(true)
|
|
152
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
153
|
-
const [isNotFound, setIsNotFound] = React.useState(false)
|
|
154
|
-
const [isSaving, setIsSaving] = React.useState(false)
|
|
155
|
-
const [editActive, setEditActive] = React.useState<boolean | null>(null)
|
|
156
|
-
const [editDisplayName, setEditDisplayName] = React.useState('')
|
|
157
|
-
const [availableRoles, setAvailableRoles] = React.useState<Array<{ id: string; name: string }>>([])
|
|
158
|
-
const [selectedRoleIds, setSelectedRoleIds] = React.useState<string[]>([])
|
|
159
|
-
const [resetPasswordOpen, setResetPasswordOpen] = React.useState(false)
|
|
160
|
-
const [isVerifying, setIsVerifying] = React.useState(false)
|
|
161
|
-
const [editPersonEntityId, setEditPersonEntityId] = React.useState<string | null>(null)
|
|
162
|
-
const [editCustomerEntityId, setEditCustomerEntityId] = React.useState<string | null>(null)
|
|
163
|
-
const [personSearchQuery, setPersonSearchQuery] = React.useState('')
|
|
164
|
-
const [companySearchQuery, setCompanySearchQuery] = React.useState('')
|
|
165
|
-
const [personResults, setPersonResults] = React.useState<Array<{ id: string; label: string }>>([])
|
|
166
|
-
const [companyResults, setCompanyResults] = React.useState<Array<{ id: string; label: string }>>([])
|
|
167
|
-
const [personName, setPersonName] = React.useState<string | null>(null)
|
|
168
|
-
const [companyName, setCompanyName] = React.useState<string | null>(null)
|
|
169
|
-
const [isSendingResetLink, setIsSendingResetLink] = React.useState(false)
|
|
170
|
-
const [resetLinkUrl, setResetLinkUrl] = React.useState<string | null>(null)
|
|
171
|
-
|
|
172
|
-
const mutationContextId = `customer_accounts:user:${id ?? 'pending'}`
|
|
173
|
-
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
174
|
-
entityType: string
|
|
175
|
-
entityId?: string
|
|
176
|
-
}>({
|
|
177
|
-
contextId: mutationContextId,
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const runMutationWithContext = React.useCallback(
|
|
181
|
-
async <T,>(operation: () => Promise<T>, mutationPayload?: Record<string, unknown>): Promise<T> => {
|
|
182
|
-
return runMutation({
|
|
183
|
-
operation,
|
|
184
|
-
mutationPayload,
|
|
185
|
-
context: { entityType: 'customer_accounts:user', entityId: id },
|
|
186
|
-
})
|
|
187
|
-
},
|
|
188
|
-
[id, runMutation],
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
React.useEffect(() => {
|
|
192
|
-
if (!id) {
|
|
193
|
-
setIsNotFound(true)
|
|
194
|
-
setIsLoading(false)
|
|
195
|
-
return
|
|
196
|
-
}
|
|
197
|
-
let cancelled = false
|
|
198
|
-
async function load() {
|
|
199
|
-
setIsLoading(true)
|
|
200
|
-
setError(null)
|
|
201
|
-
setIsNotFound(false)
|
|
202
|
-
try {
|
|
203
|
-
const payload = await readApiResultOrThrow<UserDetail>(
|
|
204
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id!)}`,
|
|
205
|
-
undefined,
|
|
206
|
-
{ errorMessage: t('customer_accounts.admin.detail.error.load', 'Failed to load user') },
|
|
207
|
-
)
|
|
208
|
-
if (cancelled) return
|
|
209
|
-
setData(payload)
|
|
210
|
-
setEditActive(payload.isActive)
|
|
211
|
-
setEditDisplayName(payload.displayName)
|
|
212
|
-
setSelectedRoleIds(payload.roles.map((role) => role.id))
|
|
213
|
-
setEditPersonEntityId(payload.personEntityId)
|
|
214
|
-
setEditCustomerEntityId(payload.customerEntityId)
|
|
215
|
-
} catch (err) {
|
|
216
|
-
if (cancelled) return
|
|
217
|
-
if ((err as { status?: number }).status === 404) {
|
|
218
|
-
setIsNotFound(true)
|
|
219
|
-
} else {
|
|
220
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.load', 'Failed to load user')
|
|
221
|
-
setError(message)
|
|
222
|
-
}
|
|
223
|
-
} finally {
|
|
224
|
-
if (!cancelled) setIsLoading(false)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
load()
|
|
228
|
-
return () => { cancelled = true }
|
|
229
|
-
}, [id, t])
|
|
230
|
-
|
|
231
|
-
React.useEffect(() => {
|
|
232
|
-
let cancelled = false
|
|
233
|
-
async function loadRoles() {
|
|
234
|
-
try {
|
|
235
|
-
const call = await apiCall<{ items?: Array<{ id: string; name: string }> }>(
|
|
236
|
-
'/api/customer_accounts/admin/roles?pageSize=100',
|
|
237
|
-
)
|
|
238
|
-
if (cancelled || !call.ok) return
|
|
239
|
-
const items = Array.isArray(call.result?.items) ? call.result!.items : []
|
|
240
|
-
setAvailableRoles(
|
|
241
|
-
items.filter((item) => typeof item?.id === 'string' && typeof item?.name === 'string'),
|
|
242
|
-
)
|
|
243
|
-
} catch {
|
|
244
|
-
// silently ignore role loading failures
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
loadRoles()
|
|
248
|
-
return () => { cancelled = true }
|
|
249
|
-
}, [])
|
|
250
|
-
|
|
251
|
-
React.useEffect(() => {
|
|
252
|
-
if (!data) return
|
|
253
|
-
let cancelled = false
|
|
254
|
-
async function loadPersonName() {
|
|
255
|
-
if (!data!.personEntityId) return
|
|
256
|
-
try {
|
|
257
|
-
const call = await apiCall<{ id?: string; firstName?: string; lastName?: string }>(`/api/customers/people/${encodeURIComponent(data!.personEntityId)}`)
|
|
258
|
-
if (!cancelled && call.ok && call.result) {
|
|
259
|
-
setPersonName([call.result.firstName, call.result.lastName].filter(Boolean).join(' ') || call.result.id || null)
|
|
260
|
-
}
|
|
261
|
-
} catch { /* ignore */ }
|
|
262
|
-
}
|
|
263
|
-
async function loadCompanyName() {
|
|
264
|
-
if (!data!.customerEntityId) return
|
|
265
|
-
try {
|
|
266
|
-
const call = await apiCall<{ id?: string; name?: string }>(`/api/customers/${encodeURIComponent(data!.customerEntityId)}`)
|
|
267
|
-
if (!cancelled && call.ok && call.result) {
|
|
268
|
-
setCompanyName(call.result.name || call.result.id || null)
|
|
269
|
-
}
|
|
270
|
-
} catch { /* ignore */ }
|
|
271
|
-
}
|
|
272
|
-
void Promise.all([loadPersonName(), loadCompanyName()])
|
|
273
|
-
return () => { cancelled = true }
|
|
274
|
-
}, [data])
|
|
275
|
-
|
|
276
|
-
const handleSearchPeople = React.useCallback(async (query: string) => {
|
|
277
|
-
setPersonSearchQuery(query)
|
|
278
|
-
if (query.trim().length < 2) { setPersonResults([]); return }
|
|
279
|
-
try {
|
|
280
|
-
const call = await apiCall<{ items?: Array<{ id: string; firstName?: string; lastName?: string; email?: string }> }>(
|
|
281
|
-
`/api/customers/people?search=${encodeURIComponent(query.trim())}&pageSize=10`,
|
|
282
|
-
)
|
|
283
|
-
if (call.ok && Array.isArray(call.result?.items)) {
|
|
284
|
-
setPersonResults(call.result!.items.map((person) => ({
|
|
285
|
-
id: person.id,
|
|
286
|
-
label: [person.firstName, person.lastName].filter(Boolean).join(' ') || person.email || person.id,
|
|
287
|
-
})))
|
|
288
|
-
}
|
|
289
|
-
} catch { /* ignore */ }
|
|
290
|
-
}, [])
|
|
291
|
-
|
|
292
|
-
const handleSearchCompanies = React.useCallback(async (query: string) => {
|
|
293
|
-
setCompanySearchQuery(query)
|
|
294
|
-
if (query.trim().length < 2) { setCompanyResults([]); return }
|
|
295
|
-
try {
|
|
296
|
-
const call = await apiCall<{ items?: Array<{ id: string; name?: string }> }>(
|
|
297
|
-
`/api/customers?search=${encodeURIComponent(query.trim())}&pageSize=10`,
|
|
298
|
-
)
|
|
299
|
-
if (call.ok && Array.isArray(call.result?.items)) {
|
|
300
|
-
setCompanyResults(call.result!.items.map((company) => ({
|
|
301
|
-
id: company.id,
|
|
302
|
-
label: company.name || company.id,
|
|
303
|
-
})))
|
|
304
|
-
}
|
|
305
|
-
} catch { /* ignore */ }
|
|
306
|
-
}, [])
|
|
307
|
-
|
|
308
|
-
const handleSendResetLink = React.useCallback(async () => {
|
|
309
|
-
if (!id) return
|
|
310
|
-
setIsSendingResetLink(true)
|
|
311
|
-
try {
|
|
312
|
-
await runMutationWithContext(async () => {
|
|
313
|
-
const call = await apiCall<{ ok: boolean; resetLink?: string; error?: string }>(
|
|
314
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id)}/send-reset-link`,
|
|
315
|
-
{ method: 'POST' },
|
|
316
|
-
)
|
|
317
|
-
if (!call.ok || !call.result?.resetLink) {
|
|
318
|
-
flash(call.result?.error || t('customer_accounts.admin.detail.sendResetLink.error', 'Failed to generate reset link'), 'error')
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
setResetLinkUrl(call.result.resetLink)
|
|
322
|
-
flash(t('customer_accounts.admin.detail.sendResetLink.flash.success', 'Reset link generated'), 'success')
|
|
323
|
-
}, { id })
|
|
324
|
-
} catch (err) {
|
|
325
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.sendResetLink.error', 'Failed to generate reset link')
|
|
326
|
-
flash(message, 'error')
|
|
327
|
-
} finally {
|
|
328
|
-
setIsSendingResetLink(false)
|
|
329
|
-
}
|
|
330
|
-
}, [id, runMutationWithContext, t])
|
|
331
|
-
|
|
332
|
-
const handleSave = React.useCallback(async () => {
|
|
333
|
-
if (!data || !id) return
|
|
334
|
-
setIsSaving(true)
|
|
335
|
-
try {
|
|
336
|
-
await runMutationWithContext(async () => {
|
|
337
|
-
const call = await withScopedApiRequestHeaders(
|
|
338
|
-
buildOptimisticLockHeader(data.updatedAt),
|
|
339
|
-
() => apiCall<{ ok: boolean; error?: string; updatedAt?: string | null }>(
|
|
340
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id)}`,
|
|
341
|
-
{
|
|
342
|
-
method: 'PUT',
|
|
343
|
-
headers: { 'content-type': 'application/json' },
|
|
344
|
-
body: JSON.stringify({
|
|
345
|
-
displayName: editDisplayName.trim() || undefined,
|
|
346
|
-
isActive: editActive,
|
|
347
|
-
roleIds: selectedRoleIds,
|
|
348
|
-
personEntityId: editPersonEntityId,
|
|
349
|
-
customerEntityId: editCustomerEntityId,
|
|
350
|
-
}),
|
|
351
|
-
},
|
|
352
|
-
),
|
|
353
|
-
)
|
|
354
|
-
if (!call.ok) {
|
|
355
|
-
if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return
|
|
356
|
-
flash(call.result?.error || t('customer_accounts.admin.detail.error.save', 'Failed to save user'), 'error')
|
|
357
|
-
return
|
|
358
|
-
}
|
|
359
|
-
flash(t('customer_accounts.admin.detail.flash.saved', 'User updated'), 'success')
|
|
360
|
-
setData((prev) => prev ? {
|
|
361
|
-
...prev,
|
|
362
|
-
isActive: editActive ?? prev.isActive,
|
|
363
|
-
displayName: editDisplayName.trim() || prev.displayName,
|
|
364
|
-
personEntityId: editPersonEntityId,
|
|
365
|
-
customerEntityId: editCustomerEntityId,
|
|
366
|
-
updatedAt: call.result?.updatedAt ?? prev.updatedAt,
|
|
367
|
-
} : prev)
|
|
368
|
-
}, { displayName: editDisplayName, isActive: editActive, roleIds: selectedRoleIds, personEntityId: editPersonEntityId, customerEntityId: editCustomerEntityId })
|
|
369
|
-
} catch (err) {
|
|
370
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.save', 'Failed to save user')
|
|
371
|
-
flash(message, 'error')
|
|
372
|
-
} finally {
|
|
373
|
-
setIsSaving(false)
|
|
374
|
-
}
|
|
375
|
-
}, [data, editActive, editCustomerEntityId, editDisplayName, editPersonEntityId, id, runMutationWithContext, selectedRoleIds, t])
|
|
376
|
-
|
|
377
|
-
const handleDelete = React.useCallback(async () => {
|
|
378
|
-
if (!data || !id) return
|
|
379
|
-
const confirmed = await confirm({
|
|
380
|
-
title: t('customer_accounts.admin.confirm.delete', 'Delete user "{{name}}"?', {
|
|
381
|
-
name: data.displayName || data.email,
|
|
382
|
-
}),
|
|
383
|
-
variant: 'destructive',
|
|
384
|
-
})
|
|
385
|
-
if (!confirmed) return
|
|
386
|
-
try {
|
|
387
|
-
await runMutationWithContext(async () => {
|
|
388
|
-
const call = await withScopedApiRequestHeaders(
|
|
389
|
-
buildOptimisticLockHeader(data.updatedAt),
|
|
390
|
-
() => apiCall(
|
|
391
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id)}`,
|
|
392
|
-
{ method: 'DELETE' },
|
|
393
|
-
),
|
|
394
|
-
)
|
|
395
|
-
if (!call.ok) {
|
|
396
|
-
if (surfaceRecordConflict({ status: call.status, body: call.result }, t)) return
|
|
397
|
-
flash(t('customer_accounts.admin.error.delete', 'Failed to delete user'), 'error')
|
|
398
|
-
return
|
|
399
|
-
}
|
|
400
|
-
flash(t('customer_accounts.admin.flash.deleted', 'User deleted'), 'success')
|
|
401
|
-
router.push('/backend/customer_accounts/users')
|
|
402
|
-
}, { id })
|
|
403
|
-
} catch (err) {
|
|
404
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.error.delete', 'Failed to delete user')
|
|
405
|
-
flash(message, 'error')
|
|
406
|
-
}
|
|
407
|
-
}, [confirm, data, id, router, runMutationWithContext, t])
|
|
408
|
-
|
|
409
|
-
const handleVerifyEmail = React.useCallback(async () => {
|
|
410
|
-
if (!data || !id) return
|
|
411
|
-
const confirmed = await confirm({
|
|
412
|
-
title: t('customer_accounts.admin.detail.verifyEmail.confirm', 'Mark email as verified for "{{name}}"?', {
|
|
413
|
-
name: data.displayName || data.email,
|
|
414
|
-
}),
|
|
415
|
-
})
|
|
416
|
-
if (!confirmed) return
|
|
417
|
-
setIsVerifying(true)
|
|
418
|
-
try {
|
|
419
|
-
await runMutationWithContext(async () => {
|
|
420
|
-
const call = await apiCall<{ ok: boolean; error?: string }>(
|
|
421
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id)}/verify-email`,
|
|
422
|
-
{ method: 'POST' },
|
|
423
|
-
)
|
|
424
|
-
if (!call.ok) {
|
|
425
|
-
flash(call.result?.error || t('customer_accounts.admin.detail.verifyEmail.error', 'Failed to verify email'), 'error')
|
|
426
|
-
return
|
|
427
|
-
}
|
|
428
|
-
flash(t('customer_accounts.admin.detail.verifyEmail.flash.success', 'Email marked as verified'), 'success')
|
|
429
|
-
setData((prev) => prev ? { ...prev, emailVerifiedAt: new Date().toISOString() } : prev)
|
|
430
|
-
}, { id })
|
|
431
|
-
} catch (err) {
|
|
432
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.verifyEmail.error', 'Failed to verify email')
|
|
433
|
-
flash(message, 'error')
|
|
434
|
-
} finally {
|
|
435
|
-
setIsVerifying(false)
|
|
436
|
-
}
|
|
437
|
-
}, [confirm, data, id, runMutationWithContext, t])
|
|
438
|
-
|
|
439
|
-
const handleRevokeSession = React.useCallback(async (sessionId: string) => {
|
|
440
|
-
if (!id) return
|
|
441
|
-
try {
|
|
442
|
-
await runMutationWithContext(async () => {
|
|
443
|
-
const call = await apiCall(
|
|
444
|
-
`/api/customer_accounts/admin/users/${encodeURIComponent(id)}/sessions/${encodeURIComponent(sessionId)}`,
|
|
445
|
-
{ method: 'DELETE' },
|
|
446
|
-
)
|
|
447
|
-
if (!call.ok) {
|
|
448
|
-
flash(t('customer_accounts.admin.detail.error.revokeSession', 'Failed to revoke session'), 'error')
|
|
449
|
-
return
|
|
450
|
-
}
|
|
451
|
-
flash(t('customer_accounts.admin.detail.flash.sessionRevoked', 'Session revoked'), 'success')
|
|
452
|
-
setData((prev) => {
|
|
453
|
-
if (!prev) return prev
|
|
454
|
-
return { ...prev, sessions: prev.sessions.filter((session) => session.id !== sessionId) }
|
|
455
|
-
})
|
|
456
|
-
}, { sessionId })
|
|
457
|
-
} catch (err) {
|
|
458
|
-
const message = err instanceof Error ? err.message : t('customer_accounts.admin.detail.error.revokeSession', 'Failed to revoke session')
|
|
459
|
-
flash(message, 'error')
|
|
460
|
-
}
|
|
461
|
-
}, [id, runMutationWithContext, t])
|
|
462
|
-
|
|
463
|
-
const handleRoleToggle = React.useCallback((roleId: string) => {
|
|
464
|
-
setSelectedRoleIds((prev) =>
|
|
465
|
-
prev.includes(roleId)
|
|
466
|
-
? prev.filter((existingId) => existingId !== roleId)
|
|
467
|
-
: [...prev, roleId],
|
|
468
|
-
)
|
|
469
|
-
}, [])
|
|
470
|
-
|
|
471
|
-
if (isLoading) {
|
|
472
|
-
return (
|
|
473
|
-
<Page>
|
|
474
|
-
<PageBody>
|
|
475
|
-
<div className="flex h-[50vh] flex-col items-center justify-center gap-2 text-muted-foreground">
|
|
476
|
-
<Spinner className="h-6 w-6" />
|
|
477
|
-
<span>{t('customer_accounts.admin.detail.loading', 'Loading user...')}</span>
|
|
478
|
-
</div>
|
|
479
|
-
</PageBody>
|
|
480
|
-
</Page>
|
|
481
|
-
)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (isNotFound) {
|
|
485
|
-
return (
|
|
486
|
-
<Page>
|
|
487
|
-
<PageBody>
|
|
488
|
-
<RecordNotFoundState
|
|
489
|
-
label={t('customer_accounts.admin.detail.error.notFound', 'User not found')}
|
|
490
|
-
backHref="/backend/customer_accounts/users"
|
|
491
|
-
backLabel={t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
|
|
492
|
-
/>
|
|
493
|
-
</PageBody>
|
|
494
|
-
</Page>
|
|
495
|
-
)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (error || !data) {
|
|
499
|
-
return (
|
|
500
|
-
<Page>
|
|
501
|
-
<PageBody>
|
|
502
|
-
<ErrorMessage
|
|
503
|
-
label={error ?? t('customer_accounts.admin.detail.error.notFound', 'User not found')}
|
|
504
|
-
action={
|
|
505
|
-
<Button asChild variant="outline" size="sm">
|
|
506
|
-
<Link href="/backend/customer_accounts/users">
|
|
507
|
-
{t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
|
|
508
|
-
</Link>
|
|
509
|
-
</Button>
|
|
510
|
-
}
|
|
511
|
-
/>
|
|
512
|
-
</PageBody>
|
|
513
|
-
</Page>
|
|
514
|
-
)
|
|
515
|
-
}
|
|
3
|
+
import { resolvePortalRequestOrigin } from '../../../../lib/portalUrl'
|
|
4
|
+
import { PortalUserDetailPageClient } from './PortalUserDetailPageClient'
|
|
516
5
|
|
|
6
|
+
export default async function CustomerUserDetailPage({ params }: { params?: { id?: string } }) {
|
|
7
|
+
const portalOrigin = resolvePortalRequestOrigin(await headers())
|
|
517
8
|
return (
|
|
518
9
|
<Page>
|
|
519
10
|
<PageBody className="space-y-6">
|
|
520
|
-
<
|
|
521
|
-
mode="detail"
|
|
522
|
-
backHref="/backend/customer_accounts/users"
|
|
523
|
-
backLabel={t('customer_accounts.admin.detail.actions.backToList', 'Back to list')}
|
|
524
|
-
title={data.displayName}
|
|
525
|
-
subtitle={data.email}
|
|
526
|
-
onDelete={() => { void handleDelete() }}
|
|
527
|
-
deleteLabel={t('customer_accounts.admin.detail.actions.delete', 'Delete')}
|
|
528
|
-
/>
|
|
529
|
-
|
|
530
|
-
<div className="rounded-lg border border-status-info-border bg-status-info-bg p-4">
|
|
531
|
-
<div className="flex items-start gap-3">
|
|
532
|
-
<div className="flex-1">
|
|
533
|
-
<h3 className="text-sm font-medium text-status-info-text">
|
|
534
|
-
{t('customer_accounts.admin.detail.portalAccess.title', 'Customer Portal Access')}
|
|
535
|
-
</h3>
|
|
536
|
-
<p className="mt-1 text-sm text-status-info-text">
|
|
537
|
-
{t('customer_accounts.admin.detail.portalAccess.description', 'This user can access the customer portal at the URL below. The portal provides self-service access to orders, invoices, quotes, and account management.')}
|
|
538
|
-
</p>
|
|
539
|
-
<p className="mt-2 text-xs text-status-info-text">
|
|
540
|
-
{t('customer_accounts.admin.detail.portalAccess.url', 'Portal URL: {url}', {
|
|
541
|
-
url: `${typeof window !== 'undefined' ? window.location.origin : ''}/[org-slug]/portal`,
|
|
542
|
-
})}
|
|
543
|
-
</p>
|
|
544
|
-
</div>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
|
|
548
|
-
<div className="grid gap-6 md:grid-cols-2">
|
|
549
|
-
<div className="rounded-lg border p-4 space-y-3">
|
|
550
|
-
<h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.info', 'User Information')}</h2>
|
|
551
|
-
<dl className="space-y-2 text-sm">
|
|
552
|
-
<div className="flex justify-between">
|
|
553
|
-
<dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.email', 'Email')}</dt>
|
|
554
|
-
<dd>{data.email}</dd>
|
|
555
|
-
</div>
|
|
556
|
-
<div className="flex justify-between items-center">
|
|
557
|
-
<dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.emailVerified', 'Email Verified')}</dt>
|
|
558
|
-
<dd className="flex items-center gap-2">
|
|
559
|
-
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${
|
|
560
|
-
data.emailVerifiedAt
|
|
561
|
-
? 'bg-status-success-bg text-status-success-text'
|
|
562
|
-
: 'bg-status-warning-bg text-status-warning-text'
|
|
563
|
-
}`}>
|
|
564
|
-
{data.emailVerifiedAt
|
|
565
|
-
? t('customer_accounts.admin.verified', 'Yes')
|
|
566
|
-
: t('customer_accounts.admin.unverified', 'No')}
|
|
567
|
-
</span>
|
|
568
|
-
{!data.emailVerifiedAt && (
|
|
569
|
-
<Button
|
|
570
|
-
variant="outline"
|
|
571
|
-
size="sm"
|
|
572
|
-
onClick={() => { void handleVerifyEmail() }}
|
|
573
|
-
disabled={isVerifying}
|
|
574
|
-
>
|
|
575
|
-
{isVerifying
|
|
576
|
-
? t('customer_accounts.admin.detail.verifyEmail.actions.verifying', 'Verifying...')
|
|
577
|
-
: t('customer_accounts.admin.detail.verifyEmail.actions.verify', 'Mark Verified')}
|
|
578
|
-
</Button>
|
|
579
|
-
)}
|
|
580
|
-
</dd>
|
|
581
|
-
</div>
|
|
582
|
-
<div className="flex justify-between">
|
|
583
|
-
<dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.lastLogin', 'Last Login')}</dt>
|
|
584
|
-
<dd>{formatDate(data.lastLoginAt, '-')}</dd>
|
|
585
|
-
</div>
|
|
586
|
-
<div className="flex justify-between">
|
|
587
|
-
<dt className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.createdAt', 'Created')}</dt>
|
|
588
|
-
<dd>{formatDate(data.createdAt, '-')}</dd>
|
|
589
|
-
</div>
|
|
590
|
-
</dl>
|
|
591
|
-
</div>
|
|
592
|
-
|
|
593
|
-
<div className="rounded-lg border p-4 space-y-3">
|
|
594
|
-
<h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.crmLinks', 'CRM Links')}</h2>
|
|
595
|
-
<div className="space-y-3 text-sm">
|
|
596
|
-
<div className="space-y-1.5">
|
|
597
|
-
<p className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.personEntity', 'Linked Person')}</p>
|
|
598
|
-
{editPersonEntityId ? (
|
|
599
|
-
<div className="flex items-center gap-2">
|
|
600
|
-
<Link href={`/backend/customers/people/${editPersonEntityId}`} className="text-primary hover:underline">
|
|
601
|
-
{personName || editPersonEntityId}
|
|
602
|
-
</Link>
|
|
603
|
-
<Button type="button" variant="outline" size="sm" onClick={() => { setEditPersonEntityId(null); setPersonName(null) }}>
|
|
604
|
-
{t('customer_accounts.admin.detail.actions.unlink', 'Unlink')}
|
|
605
|
-
</Button>
|
|
606
|
-
</div>
|
|
607
|
-
) : (
|
|
608
|
-
<div className="space-y-1">
|
|
609
|
-
<div className="relative">
|
|
610
|
-
<Input
|
|
611
|
-
type="text"
|
|
612
|
-
value={personSearchQuery}
|
|
613
|
-
onChange={(event) => { void handleSearchPeople(event.target.value) }}
|
|
614
|
-
placeholder={t('customer_accounts.admin.detail.fields.searchPerson', 'Search people by name...')}
|
|
615
|
-
/>
|
|
616
|
-
{personResults.length > 0 && (
|
|
617
|
-
<div className="absolute z-10 mt-1 w-full rounded-md border bg-background shadow-lg max-h-40 overflow-y-auto">
|
|
618
|
-
{personResults.map((person) => (
|
|
619
|
-
<button
|
|
620
|
-
key={person.id}
|
|
621
|
-
type="button"
|
|
622
|
-
className="w-full px-3 py-2 text-left text-sm hover:bg-muted"
|
|
623
|
-
onClick={() => {
|
|
624
|
-
setEditPersonEntityId(person.id)
|
|
625
|
-
setPersonName(person.label)
|
|
626
|
-
setPersonSearchQuery('')
|
|
627
|
-
setPersonResults([])
|
|
628
|
-
}}
|
|
629
|
-
>
|
|
630
|
-
{person.label}
|
|
631
|
-
</button>
|
|
632
|
-
))}
|
|
633
|
-
</div>
|
|
634
|
-
)}
|
|
635
|
-
</div>
|
|
636
|
-
</div>
|
|
637
|
-
)}
|
|
638
|
-
</div>
|
|
639
|
-
<div className="space-y-1.5">
|
|
640
|
-
<p className="text-muted-foreground">{t('customer_accounts.admin.detail.fields.customerEntity', 'Linked Company')}</p>
|
|
641
|
-
{editCustomerEntityId ? (
|
|
642
|
-
<div className="flex items-center gap-2">
|
|
643
|
-
<Link href={`/backend/customers/companies/${editCustomerEntityId}`} className="text-primary hover:underline">
|
|
644
|
-
{companyName || editCustomerEntityId}
|
|
645
|
-
</Link>
|
|
646
|
-
<Button type="button" variant="outline" size="sm" onClick={() => { setEditCustomerEntityId(null); setCompanyName(null) }}>
|
|
647
|
-
{t('customer_accounts.admin.detail.actions.unlink', 'Unlink')}
|
|
648
|
-
</Button>
|
|
649
|
-
</div>
|
|
650
|
-
) : (
|
|
651
|
-
<div className="space-y-1">
|
|
652
|
-
<div className="relative">
|
|
653
|
-
<Input
|
|
654
|
-
type="text"
|
|
655
|
-
value={companySearchQuery}
|
|
656
|
-
onChange={(event) => { void handleSearchCompanies(event.target.value) }}
|
|
657
|
-
placeholder={t('customer_accounts.admin.detail.fields.searchCompany', 'Search companies by name...')}
|
|
658
|
-
/>
|
|
659
|
-
{companyResults.length > 0 && (
|
|
660
|
-
<div className="absolute z-10 mt-1 w-full rounded-md border bg-background shadow-lg max-h-40 overflow-y-auto">
|
|
661
|
-
{companyResults.map((company) => (
|
|
662
|
-
<button
|
|
663
|
-
key={company.id}
|
|
664
|
-
type="button"
|
|
665
|
-
className="w-full px-3 py-2 text-left text-sm hover:bg-muted"
|
|
666
|
-
onClick={() => {
|
|
667
|
-
setEditCustomerEntityId(company.id)
|
|
668
|
-
setCompanyName(company.label)
|
|
669
|
-
setCompanySearchQuery('')
|
|
670
|
-
setCompanyResults([])
|
|
671
|
-
}}
|
|
672
|
-
>
|
|
673
|
-
{company.label}
|
|
674
|
-
</button>
|
|
675
|
-
))}
|
|
676
|
-
</div>
|
|
677
|
-
)}
|
|
678
|
-
</div>
|
|
679
|
-
</div>
|
|
680
|
-
)}
|
|
681
|
-
</div>
|
|
682
|
-
<p className="text-xs text-muted-foreground">
|
|
683
|
-
{t('customer_accounts.admin.detail.crmLinks.hint', 'Changes to CRM links are saved when you click Save Changes below.')}
|
|
684
|
-
</p>
|
|
685
|
-
</div>
|
|
686
|
-
</div>
|
|
687
|
-
</div>
|
|
688
|
-
|
|
689
|
-
<div className="rounded-lg border p-4 space-y-4">
|
|
690
|
-
<h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.settings', 'Account Settings')}</h2>
|
|
691
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
692
|
-
<div className="space-y-3">
|
|
693
|
-
<div className="space-y-2">
|
|
694
|
-
<label className="text-sm font-medium" htmlFor="user-display-name">
|
|
695
|
-
{t('customer_accounts.admin.detail.fields.displayName', 'Display Name')}
|
|
696
|
-
</label>
|
|
697
|
-
<Input
|
|
698
|
-
id="user-display-name"
|
|
699
|
-
type="text"
|
|
700
|
-
value={editDisplayName}
|
|
701
|
-
onChange={(event) => setEditDisplayName(event.target.value)}
|
|
702
|
-
/>
|
|
703
|
-
</div>
|
|
704
|
-
|
|
705
|
-
<SwitchField
|
|
706
|
-
id="user-active-toggle"
|
|
707
|
-
label={t('customer_accounts.admin.detail.fields.isActive', 'Active')}
|
|
708
|
-
checked={editActive ?? data.isActive}
|
|
709
|
-
onCheckedChange={(next) => setEditActive(next)}
|
|
710
|
-
/>
|
|
711
|
-
</div>
|
|
712
|
-
|
|
713
|
-
<div className="space-y-2">
|
|
714
|
-
<p className="text-sm font-medium">{t('customer_accounts.admin.detail.fields.roles', 'Roles')}</p>
|
|
715
|
-
<div className="flex flex-wrap gap-2">
|
|
716
|
-
{availableRoles.map((role) => {
|
|
717
|
-
const isSelected = selectedRoleIds.includes(role.id)
|
|
718
|
-
return (
|
|
719
|
-
<Button
|
|
720
|
-
key={role.id}
|
|
721
|
-
type="button"
|
|
722
|
-
variant="outline"
|
|
723
|
-
size="sm"
|
|
724
|
-
onClick={() => handleRoleToggle(role.id)}
|
|
725
|
-
className={`rounded-full ${
|
|
726
|
-
isSelected
|
|
727
|
-
? 'border-primary bg-primary/10 text-primary'
|
|
728
|
-
: ''
|
|
729
|
-
}`}
|
|
730
|
-
>
|
|
731
|
-
{role.name}
|
|
732
|
-
</Button>
|
|
733
|
-
)
|
|
734
|
-
})}
|
|
735
|
-
{availableRoles.length === 0 && (
|
|
736
|
-
<span className="text-sm text-muted-foreground">
|
|
737
|
-
{t('customer_accounts.admin.detail.noRolesAvailable', 'No roles available')}
|
|
738
|
-
</span>
|
|
739
|
-
)}
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
</div>
|
|
743
|
-
<div className="pt-2">
|
|
744
|
-
<Button onClick={() => { void handleSave() }} disabled={isSaving}>
|
|
745
|
-
{isSaving
|
|
746
|
-
? t('customer_accounts.admin.detail.actions.saving', 'Saving...')
|
|
747
|
-
: t('customer_accounts.admin.detail.actions.save', 'Save Changes')}
|
|
748
|
-
</Button>
|
|
749
|
-
</div>
|
|
750
|
-
</div>
|
|
751
|
-
|
|
752
|
-
<div className="rounded-lg border p-4 space-y-3">
|
|
753
|
-
<div className="flex items-center justify-between">
|
|
754
|
-
<h2 className="text-sm font-semibold">{t('customer_accounts.admin.detail.sections.security', 'Security')}</h2>
|
|
755
|
-
</div>
|
|
756
|
-
<div className="flex flex-wrap gap-2">
|
|
757
|
-
<Button variant="outline" onClick={() => setResetPasswordOpen(true)}>
|
|
758
|
-
{t('customer_accounts.admin.detail.resetPassword.actions.open', 'Reset Password')}
|
|
759
|
-
</Button>
|
|
760
|
-
<Button
|
|
761
|
-
variant="outline"
|
|
762
|
-
onClick={() => { void handleSendResetLink() }}
|
|
763
|
-
disabled={isSendingResetLink}
|
|
764
|
-
>
|
|
765
|
-
{isSendingResetLink
|
|
766
|
-
? t('customer_accounts.admin.detail.sendResetLink.actions.sending', 'Generating...')
|
|
767
|
-
: t('customer_accounts.admin.detail.sendResetLink.actions.send', 'Send Reset Link')}
|
|
768
|
-
</Button>
|
|
769
|
-
</div>
|
|
770
|
-
{resetLinkUrl && (
|
|
771
|
-
<div className="rounded-md border border-status-info-border bg-status-info-bg p-3">
|
|
772
|
-
<p className="mb-1.5 text-sm font-medium text-status-info-text">
|
|
773
|
-
{t('customer_accounts.admin.detail.sendResetLink.linkLabel', 'Password reset link (valid for 60 minutes):')}
|
|
774
|
-
</p>
|
|
775
|
-
<div className="flex items-center gap-2">
|
|
776
|
-
<code className="flex-1 break-all rounded bg-status-info-bg px-2 py-1 text-xs text-status-info-text">
|
|
777
|
-
{resetLinkUrl}
|
|
778
|
-
</code>
|
|
779
|
-
<Button
|
|
780
|
-
type="button"
|
|
781
|
-
variant="outline"
|
|
782
|
-
size="sm"
|
|
783
|
-
onClick={() => {
|
|
784
|
-
void navigator.clipboard.writeText(resetLinkUrl)
|
|
785
|
-
flash(t('customer_accounts.admin.detail.sendResetLink.flash.copied', 'Link copied to clipboard'), 'success')
|
|
786
|
-
}}
|
|
787
|
-
>
|
|
788
|
-
{t('customer_accounts.admin.detail.sendResetLink.actions.copy', 'Copy')}
|
|
789
|
-
</Button>
|
|
790
|
-
</div>
|
|
791
|
-
<p className="mt-1.5 text-xs text-status-info-text">
|
|
792
|
-
{t('customer_accounts.admin.detail.sendResetLink.hint', 'Share this link with the customer to let them set a new password.')}
|
|
793
|
-
</p>
|
|
794
|
-
</div>
|
|
795
|
-
)}
|
|
796
|
-
</div>
|
|
797
|
-
|
|
798
|
-
<div className="rounded-lg border p-4 space-y-3">
|
|
799
|
-
<h2 className="text-sm font-semibold">
|
|
800
|
-
{t('customer_accounts.admin.detail.sections.sessions', 'Active Sessions')}
|
|
801
|
-
{data.sessions.length > 0 && (
|
|
802
|
-
<span className="ml-2 text-xs font-normal text-muted-foreground">({data.sessions.length})</span>
|
|
803
|
-
)}
|
|
804
|
-
</h2>
|
|
805
|
-
{data.sessions.length === 0 ? (
|
|
806
|
-
<p className="text-sm text-muted-foreground">
|
|
807
|
-
{t('customer_accounts.admin.detail.noSessions', 'No active sessions')}
|
|
808
|
-
</p>
|
|
809
|
-
) : (
|
|
810
|
-
<div className="divide-y">
|
|
811
|
-
{data.sessions.map((session) => (
|
|
812
|
-
<div key={session.id} className="flex items-center justify-between py-2 text-sm">
|
|
813
|
-
<div className="space-y-0.5">
|
|
814
|
-
<p className="font-medium">
|
|
815
|
-
{session.ipAddress || t('customer_accounts.admin.detail.unknownIp', 'Unknown IP')}
|
|
816
|
-
</p>
|
|
817
|
-
<p className="text-xs text-muted-foreground truncate max-w-xs">
|
|
818
|
-
{session.userAgent || t('customer_accounts.admin.detail.unknownDevice', 'Unknown device')}
|
|
819
|
-
</p>
|
|
820
|
-
<p className="text-xs text-muted-foreground">
|
|
821
|
-
{t('customer_accounts.admin.detail.fields.lastUsed', 'Last used')}: {formatDate(session.lastUsedAt, '-')}
|
|
822
|
-
</p>
|
|
823
|
-
</div>
|
|
824
|
-
<Button
|
|
825
|
-
variant="outline"
|
|
826
|
-
size="sm"
|
|
827
|
-
onClick={() => { void handleRevokeSession(session.id) }}
|
|
828
|
-
>
|
|
829
|
-
{t('customer_accounts.admin.detail.actions.revoke', 'Revoke')}
|
|
830
|
-
</Button>
|
|
831
|
-
</div>
|
|
832
|
-
))}
|
|
833
|
-
</div>
|
|
834
|
-
)}
|
|
835
|
-
</div>
|
|
836
|
-
|
|
837
|
-
<ResetPasswordDialog
|
|
838
|
-
open={resetPasswordOpen}
|
|
839
|
-
onOpenChange={setResetPasswordOpen}
|
|
840
|
-
userId={id!}
|
|
841
|
-
onRunMutation={runMutationWithContext}
|
|
842
|
-
/>
|
|
11
|
+
<PortalUserDetailPageClient params={params} portalOrigin={portalOrigin} />
|
|
843
12
|
</PageBody>
|
|
844
|
-
{ConfirmDialogElement}
|
|
845
13
|
</Page>
|
|
846
14
|
)
|
|
847
15
|
}
|