Profile ve UserDetail komponentleri düzenlendi

This commit is contained in:
Sedat Öztürk 2026-05-28 01:30:13 +03:00
parent 231860e85a
commit a3e66081e9
9 changed files with 627 additions and 260 deletions

View file

@ -133,6 +133,7 @@ public class PlatformIdentityAppService : ApplicationService
return new UserInfoViewModel()
{
Id = user.Id,
TenantId = user.TenantId,
UserName = user.UserName,
Name = user.Name,
Surname = user.Surname,
@ -219,6 +220,7 @@ public class PlatformIdentityAppService : ApplicationService
user.SetLoginEndDate(UserInfo.LoginEndDate);
user.SetWorkHour(UserInfo.WorkHour);
user.SetIsActive(UserInfo.IsActive);
user.SetLastPasswordChangeTime(UserInfo.LastPasswordChangeTime);
user.SetEmailConfirmed(UserInfo.EmailConfirmed);
user.SetPhoneNumberConfirmed(UserInfo.PhoneNumberConfirmed);
@ -239,7 +241,6 @@ public class PlatformIdentityAppService : ApplicationService
user.Name = UserInfo.Name;
user.Surname = UserInfo.Surname;
user.SetPhoneNumber(UserInfo.PhoneNumber, user.PhoneNumberConfirmed);
user.SetLastPasswordChangeTime(UserInfo.LastPasswordChangeTime);
user.SetRocketUsername(UserInfo.RocketUsername);
user.SetWorkHour(UserInfo.WorkHour);
user.SetDepartmentId(UserInfo.DepartmentId);

View file

@ -22,6 +22,8 @@ const _UserDropdown = ({ className }: CommonProps) => {
const tenant = useStoreState((state) => state.abpConfig.config?.currentTenant)
const { translate } = useLocalization()
const { signOut } = useAuth()
const displayName = name || userName
const tenantName = tenant?.name
const dropdownItemList: DropdownList[] = [
{
@ -42,12 +44,20 @@ const _UserDropdown = ({ className }: CommonProps) => {
]
const UserAvatar = (
<div className={classNames(className, 'flex items-center gap-2')}>
<Avatar size={32} shape="circle" src={avatar} alt="avatar" />
<div className="hidden md:block">
<div className="text-xs">{userName}</div>
<div className="font-bold">{name}</div>
<div className="font-bold italic">{tenant?.name}</div>
<div className={classNames(className, 'flex items-center gap-2 px-1.5 py-0')}>
<Avatar size={30} shape="circle" src={avatar} alt="avatar" />
<div className="hidden max-w-[160px] flex-col items-start leading-[1.05] md:flex">
<span className="max-w-full truncate text-[12px] text-gray-800 dark:text-gray-400">
{userName}
</span>
<span className="mt-1 max-w-full truncate text-[12px] font-bold text-gray-900 dark:text-gray-100">
{displayName}
</span>
{tenantName && (
<span className="mt-1 max-w-full truncate text-[12px] font-semibold text-indigo-600 dark:text-indigo-300">
{tenantName}
</span>
)}
</div>
</div>
)
@ -55,11 +65,18 @@ const _UserDropdown = ({ className }: CommonProps) => {
return (
<Dropdown menuStyle={{ minWidth: 240 }} renderTitle={UserAvatar} placement="bottom-end">
<Dropdown.Item variant="header">
<div className="py-2 px-3 flex items-center gap-2">
<Avatar shape="circle" src={avatar} alt="avatar" />
<div>
<div className="font-bold text-gray-900 dark:text-gray-100">{name}</div>
<div className="text-xs">{email}</div>
<div className="flex items-center gap-3 px-3 py-2.5">
<Avatar size={40} shape="circle" src={avatar} alt="avatar" />
<div className="min-w-0">
<div className="truncate font-bold text-gray-900 dark:text-gray-100">
{displayName}
</div>
<div className="truncate text-xs text-gray-800 dark:text-gray-300">{email}</div>
{tenantName && (
<div className="mt-1 truncate text-xs font-semibold text-indigo-600 dark:text-indigo-300">
{tenantName}
</div>
)}
</div>
</div>
</Dropdown.Item>

View file

@ -95,6 +95,7 @@ platformApiService.interceptors.response.use(
authority: [tokenDetails?.role],
name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(),
role: 'teacher',
tenantId: tokenDetails?.tenantid,
},
})
setIsRefreshing(false)

View file

@ -21,6 +21,7 @@ export interface AuthStoreModel {
name: string
avatar?: string
role: string
tenantId?: string
}
tenant?: {
tenantId?: string
@ -59,6 +60,7 @@ export const initialState: AuthStoreModel = {
name: '',
avatar: '',
role: 'teacher',
tenantId: '',
},
tenant: {
tenantId: '',
@ -82,7 +84,8 @@ export const authModel: AuthModel = {
state.user.userName = payload.user.userName
state.user.authority = payload.user.authority
state.user.email = payload.user.email
state.user.avatar = AVATAR_URL(payload.user.id, state.tenant?.tenantId) + `?${dayjs().unix()}`
state.user.tenantId = payload.user.tenantId
state.user.avatar = AVATAR_URL(payload.user.id, state.user?.tenantId) + `?${dayjs().unix()}`
}),
signOut: action(() => ({ ...initialState })),
// signOut: action((state) => ({ ...initialState, tenantId: state.tenant?.tenantId })),

View file

@ -35,6 +35,7 @@ function useAuth() {
expiresIn: number
}) => {
const tokenDetails: any = jwtDecode(token)
signInStore({
session: { token, refreshToken, expiresIn, expire: tokenDetails?.exp, signedIn: true },
user: {
@ -44,6 +45,7 @@ function useAuth() {
authority: [tokenDetails?.role],
name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(),
role: 'teacher',
tenantId: tokenDetails?.tenantid,
},
})
}

View file

@ -5,6 +5,7 @@ import { APP_NAME } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Suspense, lazy, useState } from 'react'
import { Helmet } from 'react-helmet'
import { FaCheckCircle, FaCompressAlt, FaNode, FaUser } from 'react-icons/fa'
import { useLocation, useNavigate } from 'react-router-dom'
type AccountSetting = {
@ -35,19 +36,23 @@ const Profile = () => {
{
label: string
path: string
icon?: JSX.Element
}
> = {
general: {
label: translate('::Abp.Identity.Profile.General'),
path: 'general',
icon: <FaUser />,
},
password: {
label: translate('::Abp.Identity.Password'),
path: 'password',
icon: <FaCheckCircle />,
},
notificationSettings: {
label: translate('::Abp.Identity.NotificationSettings'),
path: 'notification-settings',
icon: <FaCompressAlt />,
},
}
@ -78,6 +83,9 @@ const Profile = () => {
<TabList>
{Object.keys(settingsMenu).map((key) => (
<TabNav key={key} value={key}>
{settingsMenu[key].icon && (
<span className="mr-2">{settingsMenu[key].icon}</span>
)}
{settingsMenu[key].label}
</TabNav>
))}

View file

@ -1,13 +1,12 @@
import { Input, Upload } from '@/components/ui'
import { Input, Select, Upload } from '@/components/ui'
import Button from '@/components/ui/Button'
import { FormContainer } from '@/components/ui/Form'
import { FormContainer, FormItem } from '@/components/ui/Form'
import Notification from '@/components/ui/Notification'
import toast from '@/components/ui/toast'
import { AVATAR_URL } from '@/constants/app.constant'
import { useStoreActions, useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs'
import type { FieldProps } from 'formik'
import { Field, Form, Formik } from 'formik'
import { useEffect, useRef, useState } from 'react'
@ -26,13 +25,15 @@ import {
FaUserCircle,
FaPhone,
FaPlus,
FaHome,
FaUniversity,
} from 'react-icons/fa'
import * as Yup from 'yup'
import isEmpty from 'lodash/isEmpty'
import FormRow from '@/views/shared/FormRow'
import FormDesription from '@/views/shared/FormDesription'
import { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
import { getProfile, updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service'
import { SelectBoxOption } from '@/types/shared'
const schema = Yup.object().shape({
name: Yup.string().min(3).max(50).required(),
@ -44,6 +45,7 @@ const General = () => {
const [profileData, setProfileData] = useState<ProfileDto>()
const [formData, setFormData] = useState<UpdateProfileDto>()
const [image, setImage] = useState<string | undefined>()
const [countries, setCountries] = useState<CountryDto[]>([])
const auth = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user)
@ -54,8 +56,9 @@ const General = () => {
const fetchData = async () => {
setLoading(true)
const response = await getProfile()
const [response, countryResponse] = await Promise.all([getProfile(), getCountry()])
setProfileData(response.data)
setCountries(countryResponse.data)
setImage(auth.user.avatar)
setFormData({
name: response.data.name,
@ -150,6 +153,69 @@ const General = () => {
}
})
const getProfileExtraValue = (key: string) => {
const extraProperties = profileData?.extraProperties
const value =
extraProperties?.[key] ?? extraProperties?.[`${key.charAt(0).toLowerCase()}${key.slice(1)}`]
return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined
}
const getSelectValue = (options: SelectBoxOption[], value?: string) => {
if (!value) {
return null
}
return options.find((option) => option.value === value) ?? { value, label: value }
}
const nationalityOptions: SelectBoxOption[] = countries.map((country) => ({
value: country.name,
label: country.name,
}))
const educationOptions: SelectBoxOption[] = [
{
value: 'İlkokul',
label: translate('::App.EducationLevel.Primary') || 'İlkokul',
},
{
value: 'Ortaokul',
label: translate('::App.EducationLevel.MiddleSchool') || 'Ortaokul',
},
{
value: 'Lise',
label: translate('::App.EducationLevel.HighSchool') || 'Lise',
},
{
value: 'Ön Lisans',
label: translate('::App.EducationLevel.Associate') || 'Ön Lisans',
},
{
value: 'Lisans',
label: translate('::App.EducationLevel.Bachelor') || 'Lisans',
},
{
value: 'Yüksek Lisans',
label: translate('::App.EducationLevel.Master') || 'Yüksek Lisans',
},
{
value: 'Doktora',
label: translate('::App.EducationLevel.PhD') || 'Doktora',
},
]
const bloodTypeOptions: SelectBoxOption[] = [
{ value: 'A Rh+', label: 'A Rh+' },
{ value: 'A Rh-', label: 'A Rh-' },
{ value: 'B Rh+', label: 'B Rh+' },
{ value: 'B Rh-', label: 'B Rh-' },
{ value: 'AB Rh+', label: 'AB Rh+' },
{ value: 'AB Rh-', label: 'AB Rh-' },
{ value: '0 Rh+', label: '0 Rh+' },
{ value: '0 Rh-', label: '0 Rh-' },
]
if (loading) {
return <></>
}
@ -165,134 +231,173 @@ const General = () => {
}}
>
{({ touched, errors, isSubmitting, resetForm }) => {
const validatorProps = { touched, errors }
return (
<Form>
<FormContainer>
<FormDesription
title={translate('::Abp.Identity.Profile.General')}
desc={translate('::Abp.Identity.Profile.General.Description')}
/>
<FormRow
name="email"
label={translate('::Abp.Account.EmailAddress')}
{...validatorProps}
>
<Input
type="text"
disabled
prefix={<FaEnvelope className="text-xl" />}
value={profileData?.email}
></Input>
</FormRow>
<FormRow
name="phoneNumber"
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
{...validatorProps}
>
<Input
type="text"
disabled
prefix={<FaPhone className="text-xl" />}
value={profileData?.phoneNumber}
></Input>
</FormRow>
<FormRow name="phoneNumber" label={translate('::RocketUsername')} {...validatorProps}>
<Input
type="text"
disabled
prefix={<FaFacebookMessenger className="text-xl" />}
value={profileData?.extraProperties?.['RocketUsername'] as string | undefined}
></Input>
</FormRow>
<FormRow
name="name"
label={translate('::Abp.Identity.User.UserInformation.Name')}
{...validatorProps}
>
<Field
type="text"
autoComplete="off"
name="name"
placeholder="Name"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormRow>
<FormRow
name="surname"
label={translate('::Abp.Identity.User.UserInformation.Surname')}
{...validatorProps}
>
<Field
type="text"
autoComplete="off"
name="surname"
placeholder="Last Name"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormRow>
<FormRow name="avatar" label="Avatar" alignCenter={false} {...validatorProps}>
<Field name="avatar">
{({ field, form }: FieldProps) => {
return (
<>
<div>
<div className="flex flex-col lg:flex-row items-start gap-5">
{image ? (
<Cropper
ref={cropperRef}
src={image}
onUpdate={(cropper) => {
setTimeout(() => {
previewRef.current?.update(cropper)
}, 100)
}}
className="cropper max-h-[300px] max-w-[300px]"
stencilComponent={CircleStencil}
minHeight={100}
minWidth={100}
/>
) : (
<img
className="cropper max-h-[300px] max-w-[300px]"
src={auth.user.avatar}
/>
)}
<div className="flex flex-row gap-4">
<div className="flex flex-col">
<Upload
className="cursor-pointer"
showList={false}
multiple={false}
uploadLimit={1}
beforeUpload={beforeUpload}
onChange={onChooseImage}
>
<Button icon={<FaPlus />} type="button"></Button>
</Upload>
<Button
type="button"
className="my-2"
icon={<FaTrashAlt />}
onClick={() => setImage(undefined)}
></Button>
</div>
{image && (
<CropperPreview
ref={previewRef}
className="preview max-w-[100px] avatar-img avatar-circle border border-gray-400"
/>
)}
</div>
</div>
<FormContainer size="md">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
<div>
<h6 className="mb-4">
{translate('::Abp.Identity.User.UserInformation.ContactInformation')}
</h6>
<FormItem label={translate('::Abp.Account.EmailAddress')}>
<Input
type="text"
disabled
prefix={<FaEnvelope className="text-xl" />}
value={profileData?.email}
></Input>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.Name')}
invalid={(errors.name && touched.name) as boolean}
errorMessage={errors.name}
>
<Field
type="text"
autoComplete="off"
name="name"
placeholder="Name"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.Surname')}
invalid={(errors.surname && touched.surname) as boolean}
errorMessage={errors.surname}
>
<Field
type="text"
autoComplete="off"
name="surname"
placeholder="Last Name"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormItem>
<FormItem label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}>
<Input
type="text"
disabled
prefix={<FaPhone className="text-xl" />}
value={profileData?.phoneNumber}
></Input>
</FormItem>
<FormItem label={translate('::RocketUsername')}>
<Input
type="text"
disabled
prefix={<FaFacebookMessenger className="text-xl" />}
value={profileData?.extraProperties?.['RocketUsername'] as string | undefined}
></Input>
</FormItem>
</div>
<div>
<h6 className="mb-4">
{translate('::Abp.Identity.User.UserInformation.AdditionalInformation')}
</h6>
<FormItem label={translate('::Abp.Account.HomeAddress')}>
<Input
type="text"
disabled
prefix={<FaHome className="text-xl" />}
value={getProfileExtraValue('HomeAddress')}
></Input>
</FormItem>
<FormItem label={translate('::Abp.Account.Nationality')}>
<Select<SelectBoxOption>
isDisabled
options={nationalityOptions}
value={getSelectValue(
nationalityOptions,
getProfileExtraValue('Nationality'),
)}
/>
</FormItem>
<FormItem label={translate('::Abp.Account.EducationLevel')}>
<Select<SelectBoxOption>
isDisabled
options={educationOptions}
value={getSelectValue(
educationOptions,
getProfileExtraValue('EducationLevel'),
)}
/>
</FormItem>
<FormItem label={translate('::Abp.Account.GraduationSchool')}>
<Input
type="text"
disabled
prefix={<FaUniversity className="text-xl" />}
value={getProfileExtraValue('GraduationSchool')}
></Input>
</FormItem>
<FormItem label={translate('::Abp.Account.BloodType')}>
<Select<SelectBoxOption>
isDisabled
options={bloodTypeOptions}
value={getSelectValue(bloodTypeOptions, getProfileExtraValue('BloodType'))}
/>
</FormItem>
</div>
<div>
<h6 className="mb-4">Avatar</h6>
<FormItem>
<div className="flex flex-col gap-4">
{image ? (
<Cropper
ref={cropperRef}
src={image}
onUpdate={(cropper) => {
setTimeout(() => {
previewRef.current?.update(cropper)
}, 100)
}}
className="cropper max-h-[300px] max-w-[300px]"
stencilComponent={CircleStencil}
minHeight={100}
minWidth={100}
/>
) : (
<img
className="cropper max-h-[300px] max-w-[300px]"
src={auth.user.avatar}
/>
)}
<div className="flex items-start gap-4">
<div className="flex flex-col gap-2">
<Upload
className="cursor-pointer"
showList={false}
multiple={false}
uploadLimit={1}
beforeUpload={beforeUpload}
onChange={onChooseImage}
>
<Button icon={<FaPlus />} type="button"></Button>
</Upload>
<Button
type="button"
icon={<FaTrashAlt />}
onClick={() => setImage(undefined)}
></Button>
</div>
</>
)
}}
</Field>
</FormRow>
{image && (
<CropperPreview
ref={previewRef}
className="preview max-w-[100px] avatar-img avatar-circle"
/>
)}
</div>
</div>
</FormItem>
</div>
</div>
<div className="mt-4 ltr:text-right">
<Button className="ltr:mr-2 rtl:ml-2" type="button" onClick={() => resetForm()}>
{translate('::Cancel')}

View file

@ -9,6 +9,7 @@ import {
Select,
Tabs,
toast,
Upload,
} from '@/components/ui'
import Dialog from '@/components/ui/Dialog'
import DateTimepicker from '@/components/ui/DatePicker/DateTimepicker'
@ -29,11 +30,12 @@ import {
putUserLookout,
putUserPermission,
} from '@/services/identity.service'
import { updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs'
import { Field, FieldArray, FieldProps, Form, Formik, FormikHelpers } from 'formik'
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import {
FaBuilding,
@ -43,13 +45,42 @@ import {
FaTrashAlt,
FaCheckCircle,
FaUserAstronaut,
FaEnvelope,
FaPhone,
FaUserCircle,
FaFacebookMessenger,
FaHome,
FaUniversity,
FaCalendarAlt,
FaBriefcase,
FaIdCard,
FaMapMarkerAlt,
FaCity,
FaBook,
FaHashtag,
FaMapPin,
FaUserTie,
FaFemale,
FaHeart,
FaExclamationTriangle,
FaUsers,
FaPlus,
} from 'react-icons/fa'
import {
CircleStencil,
Cropper,
CropperPreview,
CropperPreviewRef,
CropperRef,
} from 'react-advanced-cropper'
import 'react-advanced-cropper/dist/style.css'
import { useParams } from 'react-router-dom'
import * as Yup from 'yup'
import { SelectBoxOption, SelectBoxOptionWithGroup } from '@/types/shared'
import { ConfirmDialog, Container } from '@/components/shared'
import { AdaptableCard, ConfirmDialog, Container } from '@/components/shared'
import { AssignedClaimViewModel, UserInfoViewModel } from '@/proxy/admin/models'
import { APP_NAME } from '@/constants/app.constant'
import { APP_NAME, AVATAR_URL } from '@/constants/app.constant'
import { useStoreActions, useStoreState } from '@/store'
export interface ClaimTypeDto {
claimType: string
@ -64,10 +95,18 @@ function UserDetails() {
const [open, setOpen] = useState(false)
const [confirmDeleteClaim, setConfirmDeleteClaim] = useState<AssignedClaimViewModel | null>(null)
const [countries, setCountries] = useState<CountryDto[]>([])
const [image, setImage] = useState<string | undefined>()
const cropperRef = useRef<CropperRef>(null)
const previewRef = useRef<CropperPreviewRef>(null)
const auth = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user)
const getUser = async () => {
const getUser = async (syncAvatar = true) => {
const { data } = await getUserDetail(userId || '')
setUserDetails(data)
if (syncAvatar) {
setImage(`${AVATAR_URL(data.id, data.tenantId)}?${dayjs().unix()}`)
}
}
useEffect(() => {
@ -80,6 +119,58 @@ function UserDetails() {
claimValue: Yup.string().required(),
})
const onChooseImage = async (file: File[]) => {
if (file[0]) {
setImage(URL.createObjectURL(file[0]))
} else {
setImage(undefined)
}
}
const beforeUpload = (files: FileList | null, fileList: File[]) => {
let valid: string | boolean = true
const allowedFileType = ['image/jpeg', 'image/png', 'image/gif']
const maxFileSize = 2000000
if (fileList.length >= 1) {
return `Sadece bir dosya seçebilirsiniz`
}
if (files) {
for (const f of files) {
if (!allowedFileType.includes(f.type)) {
valid = '.jpg, .jpeg, .gif veya .png yükleyebilirsiniz'
} else if (f.size >= maxFileSize) {
valid = 'En fazla 2mb dosya yükleyebilirsiniz'
}
}
}
return valid
}
const getCroppedAvatar = () =>
new Promise<Blob | null>((resolve, reject) => {
const canvas = cropperRef.current?.getCanvas({
minHeight: 100,
minWidth: 100,
maxHeight: 300,
maxWidth: 300,
})
if (canvas) {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject()
}
}, 'image/jpeg')
} else {
resolve(null)
}
})
const handleSubmit = async (
values: ClaimTypeDto,
{ setSubmitting }: FormikHelpers<ClaimTypeDto>,
@ -121,35 +212,60 @@ function UserDetails() {
title={userDetails.email}
defaultTitle={APP_NAME}
></Helmet>
<Container>
<AdaptableCard>
<Tabs defaultValue="user">
<TabList>
<TabNav value="user" icon={<FaUser />}>
<TabNav value="user" icon={<FaUser className="text-sm" />}>
{translate('::Abp.Identity.User.UserInformation')}
</TabNav>
<TabNav value="permission" icon={<FaCheckCircle />}>
<TabNav value="permission" icon={<FaCheckCircle className="text-sm" />}>
{translate('::Abp.Identity.User.Permissions')}
</TabNav>
<TabNav value="work" icon={<FaBuilding />}>
<TabNav value="work" icon={<FaBuilding className="text-sm" />}>
{translate('::Abp.Identity.User.WorkInformation')}
</TabNav>
<TabNav value="identity" icon={<FaUserAstronaut />}>
<TabNav value="identity" icon={<FaUserAstronaut className="text-sm" />}>
{translate('::Abp.Identity.User.IndentityInformation')}
</TabNav>
<TabNav value="lockout" icon={<FaLockOpen />}>
<TabNav value="lockout" icon={<FaLockOpen className="text-sm" />}>
{translate('::Abp.Identity.User.LockoutManagement')}
</TabNav>
<TabNav value="claimTypes" icon={<FaFileAlt />}>
<TabNav value="claimTypes" icon={<FaFileAlt className="text-sm" />}>
{translate('::Abp.Identity.User.ClaimTypes')}
</TabNav>
</TabList>
<TabContent value="user">
<div className="mt-5">
<div className="px-4 py-6">
<Formik
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
setSubmitting(true)
await putUserDetail({ ...values })
let keepCurrentAvatar = false
const avatar = await getCroppedAvatar()
const resp = await updateProfile({
name: values.name ?? '',
surname: values.surname ?? '',
avatar: avatar ? new File([avatar], 'avatar') : undefined,
})
if (resp.status === 200) {
const avatarUrl =
AVATAR_URL(auth.user.id, auth.tenant?.tenantId) + `?${dayjs().unix()}`
setUser({
...auth.user,
name: `${resp.data.name} ${resp.data.surname}`.trim(),
avatar: avatarUrl,
})
setImage(avatarUrl)
keepCurrentAvatar = true
} else {
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
placement: 'top-end',
})
}
toast.push(
<Notification type="success" duration={2000}>
@ -160,7 +276,7 @@ function UserDetails() {
},
)
getUser()
getUser(!keepCurrentAvatar)
setSubmitting(false)
}}
>
@ -169,7 +285,7 @@ function UserDetails() {
<Form>
<div>
<FormContainer size="md">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
{/* Personal Information */}
<div>
<h6 className="mb-4">
@ -177,6 +293,17 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.ContactInformation',
)}
</h6>
<FormItem label={translate('::Abp.Account.EmailAddress')}>
<Field
type="text"
disabled
name="email"
placeholder="Email Address"
prefix={<FaEnvelope className="text-xl" />}
component={Input}
/>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.Name')}
>
@ -185,6 +312,7 @@ function UserDetails() {
name="name"
placeholder="Name"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormItem>
@ -196,18 +324,10 @@ function UserDetails() {
name="surname"
placeholder="Surname"
component={Input}
prefix={<FaUserCircle className="text-xl" />}
/>
</FormItem>
<FormItem label={translate('::Abp.Account.EmailAddress')}>
<Field
type="text"
disabled
name="email"
placeholder="Email Address"
component={Input}
/>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
>
@ -217,27 +337,18 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.PhoneNumber',
)}
prefix={<FaPhone className="text-xl" />}
component={Input}
/>
</FormItem>
</div>
<div>
<h6 className="mb-4">&nbsp;</h6>
<FormItem label={translate('::Abp.Account.HomeAddress')}>
<Field
type="text"
name="homeAddress"
placeholder={translate('::Abp.Account.HomeAddress')}
component={Input}
/>
</FormItem>
<FormItem size="sm" label={translate('::RocketUsername')}>
<Field
type="text"
name="rocketUsername"
placeholder={translate('::RocketUsername')}
component={Input}
prefix={<FaFacebookMessenger className="text-xl" />}
/>
</FormItem>
</div>
@ -248,6 +359,16 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.AdditionalInformation',
)}
</h6>
<FormItem label={translate('::Abp.Account.HomeAddress')}>
<Field
type="text"
name="homeAddress"
placeholder={translate('::Abp.Account.HomeAddress')}
component={Input}
prefix={<FaHome className="text-xl" />}
/>
</FormItem>
<FormItem label={translate('::Abp.Account.Nationality')}>
<Field type="text" name="nationality">
{({ field, form }: FieldProps<SelectBoxOption>) => {
@ -330,12 +451,14 @@ function UserDetails() {
}}
</Field>
</FormItem>
<FormItem label={translate('::Abp.Account.GraduationSchool')}>
<Field
type="text"
name="graduationSchool"
placeholder={translate('::GraduationSchool')}
component={Input}
prefix={<FaUniversity className="text-xl" />}
/>
</FormItem>
<FormItem label={translate('::Abp.Account.BloodType')}>
@ -371,76 +494,73 @@ function UserDetails() {
</div>
<div>
<h6 className="mb-4">
{translate('::Abp.Identity.User.UserInformation.AccountTimestamps')}
</h6>
<FormItem
label={translate(
'::Abp.Identity.User.UserInformation.PasswordChangeTime',
)}
>
<Field name="lastPasswordChangeTime">
{({ field, form }: FieldProps) => (
<DateTimepicker
inputFormat="DD/MM/YYYY HH:mm"
field={field}
form={form}
value={field.value ? dayjs(field.value).toDate() : null}
placeholder="Select Date"
onChange={(date: any) => {
form.setFieldValue(
field.name,
date ? dayjs(date).format('YYYY-MM-DDTHH:mm:ss') : null,
)
<h6 className="mb-4">Avatar</h6>
<FormItem>
<div className="flex flex-col gap-4">
{image ? (
<Cropper
ref={cropperRef}
src={image}
onUpdate={(cropper) => {
setTimeout(() => {
previewRef.current?.update(cropper)
}, 100)
}}
className="cropper max-h-[300px] max-w-[300px]"
stencilComponent={CircleStencil}
minHeight={100}
minWidth={100}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.CreateTime')}
>
<Field name="creationTime">
{({ field, form }: FieldProps) => (
<Input
field={field}
form={form}
value={
field.value
? dayjs(field.value).format('DD/MM/YYYY HH:mm')
: undefined
) : (
<img
className="cropper max-h-[300px] max-w-[300px]"
src={
image ||
AVATAR_URL(
values.id,
values.tenantId,
)
}
disabled
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::Abp.Identity.User.UserInformation.UpdateTime')}
>
<Field name="lastModificationTime">
{({ field, form }: FieldProps) => (
<Input
field={field}
form={form}
value={
field.value
? dayjs(field.value).format('DD/MM/YYYY HH:mm')
: undefined
}
disabled
/>
)}
</Field>
<div className="flex items-start gap-4">
<div className="flex flex-col gap-2">
<Upload
className="cursor-pointer"
showList={false}
multiple={false}
uploadLimit={1}
beforeUpload={beforeUpload}
onChange={onChooseImage}
>
<Button icon={<FaPlus />} type="button"></Button>
</Upload>
<Button
type="button"
icon={<FaTrashAlt />}
onClick={() => setImage(undefined)}
></Button>
</div>
{image && (
<CropperPreview
ref={previewRef}
className="preview max-w-[100px] avatar-img avatar-circle"
/>
)}
</div>
</div>
</FormItem>
</div>
</div>
</FormContainer>
</div>
<Button variant="solid" block loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
<div className="mt-4 ltr:text-right">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form>
)
}}
@ -449,7 +569,7 @@ function UserDetails() {
</TabContent>
<TabContent value="permission">
<div className="mt-5">
<div className="px-4 py-6">
<Formik
initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => {
@ -547,8 +667,8 @@ function UserDetails() {
</FormContainer>
</div>
<div className="mt-4">
<Button variant="solid" block loading={isSubmitting} type="submit">
<div className="mt-4 ltr:text-right">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
@ -560,7 +680,7 @@ function UserDetails() {
</TabContent>
<TabContent value="work">
<div className="mt-5">
<div className="px-4 py-6">
<Formik
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
@ -601,7 +721,7 @@ function UserDetails() {
<Form>
<div>
<FormContainer size="md">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
<div>
<FormItem
label={translate(
@ -666,6 +786,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SskNo',
)}
component={Input}
prefix={<FaIdCard className="text-xl" />}
/>
</FormItem>
</div>
@ -683,6 +804,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.HireDate',
)}
inputPrefix={<FaBriefcase className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -710,6 +832,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.TerminationDate',
)}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -725,9 +848,11 @@ function UserDetails() {
</FormContainer>
</div>
<Button variant="solid" block loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
<div className="mt-4 ltr:text-right">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form>
)
}}
@ -736,7 +861,7 @@ function UserDetails() {
</TabContent>
<TabContent value="identity">
<div className="mt-5">
<div className="px-4 py-6">
<Formik
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
@ -779,7 +904,7 @@ function UserDetails() {
return (
<Form>
<FormContainer size="md">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-5 w-full">
<div>
<FormItem
label={translate(
@ -793,6 +918,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.IdentityNumber',
)}
component={Input}
prefix={<FaIdCard className="text-xl" />}
/>
</FormItem>
</div>
@ -808,6 +934,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SerialNo',
)}
component={Input}
prefix={<FaHashtag className="text-xl" />}
/>
</FormItem>
</div>
@ -823,6 +950,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.Province',
)}
component={Input}
prefix={<FaMapMarkerAlt className="text-xl" />}
/>
</FormItem>
</div>
@ -838,6 +966,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.District',
)}
component={Input}
prefix={<FaCity className="text-xl" />}
/>
</FormItem>
</div>
@ -853,6 +982,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.Village',
)}
component={Input}
prefix={<FaHome className="text-xl" />}
/>
</FormItem>
</div>
@ -868,6 +998,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.VolumeNo',
)}
component={Input}
prefix={<FaBook className="text-xl" />}
/>
</FormItem>
</div>
@ -885,6 +1016,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.FamilySequenceNo',
)}
component={Input}
prefix={<FaUsers className="text-xl" />}
/>
</FormItem>
</div>
@ -900,6 +1032,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SequenceNo',
)}
component={Input}
prefix={<FaHashtag className="text-xl" />}
/>
</FormItem>
</div>
@ -915,6 +1048,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.IssuedPlace',
)}
component={Input}
prefix={<FaMapPin className="text-xl" />}
/>
</FormItem>
</div>
@ -932,6 +1066,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.IssuedDate',
)}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -955,6 +1090,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.BirthPlace',
)}
component={Input}
prefix={<FaMapMarkerAlt className="text-xl" />}
/>
</FormItem>
</div>
@ -972,6 +1108,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.BirthDate',
)}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -995,6 +1132,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.FatherName',
)}
component={Input}
prefix={<FaUserTie className="text-xl" />}
/>
</FormItem>
</div>
@ -1010,6 +1148,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.MotherName',
)}
component={Input}
prefix={<FaFemale className="text-xl" />}
/>
</FormItem>
</div>
@ -1050,6 +1189,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.UserInformation.MarriageDate',
)}
inputPrefix={<FaHeart className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -1064,9 +1204,11 @@ function UserDetails() {
</div>
</FormContainer>
<Button variant="solid" block loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
<div className="mt-4 ltr:text-right">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form>
)
}}
@ -1075,7 +1217,7 @@ function UserDetails() {
</TabContent>
<TabContent value="lockout">
<div className="mt-5">
<div className="px-4 py-6">
<Formik
initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => {
@ -1108,7 +1250,7 @@ function UserDetails() {
return (
<Form>
<FormContainer size="md">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 w-full">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
{/* Account Status */}
<div>
<h6 className="mb-4">
@ -1258,6 +1400,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.LockoutManagement.AccountEndDate',
)}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -1295,7 +1438,12 @@ function UserDetails() {
'::Abp.Identity.User.LockoutManagement.AccessFailedCount',
)}
>
<Field type="number" name="accessFailedCount" component={Input} />
<Field
type="number"
name="accessFailedCount"
component={Input}
prefix={<FaExclamationTriangle className="text-xl" />}
/>
</FormItem>
</div>
@ -1346,6 +1494,7 @@ function UserDetails() {
placeholder={translate(
'::Abp.Identity.User.LockoutManagement.LoginEndDate',
)}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => {
form.setFieldValue(
field.name,
@ -1356,13 +1505,88 @@ function UserDetails() {
)}
</Field>
</FormItem>
<FormItem
layout="horizontal"
labelClass="!justify-start"
labelWidth="50%"
label={translate(
'::Abp.Identity.User.UserInformation.PasswordChangeTime',
)}
>
<Field name="lastPasswordChangeTime">
{({ field, form }: FieldProps) => (
<DateTimepicker
inputFormat="DD/MM/YYYY HH:mm"
field={field}
form={form}
value={field.value ? dayjs(field.value).toDate() : null}
placeholder="Select Date"
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date: any) => {
form.setFieldValue(
field.name,
date ? dayjs(date).format('YYYY-MM-DDTHH:mm:ss') : null,
)
}}
/>
)}
</Field>
</FormItem>
<FormItem
layout="horizontal"
labelClass="!justify-start"
labelWidth="50%"
label={translate('::Abp.Identity.User.UserInformation.CreateTime')}
>
<Field name="creationTime">
{({ field, form }: FieldProps) => (
<Input
field={field}
form={form}
value={
field.value
? dayjs(field.value).format('DD/MM/YYYY HH:mm')
: undefined
}
disabled
prefix={<FaCalendarAlt className="text-xl" />}
/>
)}
</Field>
</FormItem>
<FormItem
layout="horizontal"
labelClass="!justify-start"
labelWidth="50%"
label={translate('::Abp.Identity.User.UserInformation.UpdateTime')}
>
<Field name="lastModificationTime">
{({ field, form }: FieldProps) => (
<Input
field={field}
form={form}
value={
field.value
? dayjs(field.value).format('DD/MM/YYYY HH:mm')
: undefined
}
disabled
prefix={<FaCalendarAlt className="text-xl" />}
/>
)}
</Field>
</FormItem>
</div>
</div>
</FormContainer>
<Button variant="solid" block loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
<div className="mt-4 ltr:text-right">
<Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form>
)
}}
@ -1371,7 +1595,7 @@ function UserDetails() {
</TabContent>
<TabContent value="claimTypes">
<div className="mt-5">
<div className="px-4 py-6">
<Table compact>
<THead>
<Tr>
@ -1424,7 +1648,7 @@ function UserDetails() {
</div>
</TabContent>
</Tabs>
</Container>
</AdaptableCard>
<Dialog isOpen={open} onClose={() => setOpen(false)} onRequestClose={() => setOpen(false)}>
<Formik
@ -1465,7 +1689,13 @@ function UserDetails() {
invalid={errors.claimValue && touched.claimValue}
errorMessage={errors.claimValue}
>
<Field type="text" autoComplete="off" name="claimValue" component={Input} />
<Field
type="text"
autoComplete="off"
name="claimValue"
component={Input}
prefix={<FaFileAlt className="text-xl" />}
/>
</FormItem>
<div className="mt-6 flex flex-row justify-end gap-3">