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

View file

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

View file

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

View file

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

View file

@ -35,6 +35,7 @@ function useAuth() {
expiresIn: number expiresIn: number
}) => { }) => {
const tokenDetails: any = jwtDecode(token) const tokenDetails: any = jwtDecode(token)
signInStore({ signInStore({
session: { token, refreshToken, expiresIn, expire: tokenDetails?.exp, signedIn: true }, session: { token, refreshToken, expiresIn, expire: tokenDetails?.exp, signedIn: true },
user: { user: {
@ -44,6 +45,7 @@ function useAuth() {
authority: [tokenDetails?.role], authority: [tokenDetails?.role],
name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(), name: `${tokenDetails?.given_name} ${tokenDetails?.family_name}`.trim(),
role: 'teacher', 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 { useLocalization } from '@/utils/hooks/useLocalization'
import { Suspense, lazy, useState } from 'react' import { Suspense, lazy, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { FaCheckCircle, FaCompressAlt, FaNode, FaUser } from 'react-icons/fa'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
type AccountSetting = { type AccountSetting = {
@ -35,19 +36,23 @@ const Profile = () => {
{ {
label: string label: string
path: string path: string
icon?: JSX.Element
} }
> = { > = {
general: { general: {
label: translate('::Abp.Identity.Profile.General'), label: translate('::Abp.Identity.Profile.General'),
path: 'general', path: 'general',
icon: <FaUser />,
}, },
password: { password: {
label: translate('::Abp.Identity.Password'), label: translate('::Abp.Identity.Password'),
path: 'password', path: 'password',
icon: <FaCheckCircle />,
}, },
notificationSettings: { notificationSettings: {
label: translate('::Abp.Identity.NotificationSettings'), label: translate('::Abp.Identity.NotificationSettings'),
path: 'notification-settings', path: 'notification-settings',
icon: <FaCompressAlt />,
}, },
} }
@ -78,6 +83,9 @@ const Profile = () => {
<TabList> <TabList>
{Object.keys(settingsMenu).map((key) => ( {Object.keys(settingsMenu).map((key) => (
<TabNav key={key} value={key}> <TabNav key={key} value={key}>
{settingsMenu[key].icon && (
<span className="mr-2">{settingsMenu[key].icon}</span>
)}
{settingsMenu[key].label} {settingsMenu[key].label}
</TabNav> </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 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 Notification from '@/components/ui/Notification'
import toast from '@/components/ui/toast' import toast from '@/components/ui/toast'
import { AVATAR_URL } from '@/constants/app.constant' import { AVATAR_URL } from '@/constants/app.constant'
import { useStoreActions, useStoreState } from '@/store' import { useStoreActions, useStoreState } from '@/store'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { FieldProps } from 'formik'
import { Field, Form, Formik } from 'formik' import { Field, Form, Formik } from 'formik'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
@ -26,13 +25,15 @@ import {
FaUserCircle, FaUserCircle,
FaPhone, FaPhone,
FaPlus, FaPlus,
FaHome,
FaUniversity,
} from 'react-icons/fa' } from 'react-icons/fa'
import * as Yup from 'yup' import * as Yup from 'yup'
import isEmpty from 'lodash/isEmpty' 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 { ProfileDto, UpdateProfileDto } from '@/proxy/account/models'
import { getProfile, updateProfile } from '@/services/account.service' import { getProfile, updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service'
import { SelectBoxOption } from '@/types/shared'
const schema = Yup.object().shape({ const schema = Yup.object().shape({
name: Yup.string().min(3).max(50).required(), name: Yup.string().min(3).max(50).required(),
@ -44,6 +45,7 @@ const General = () => {
const [profileData, setProfileData] = useState<ProfileDto>() const [profileData, setProfileData] = useState<ProfileDto>()
const [formData, setFormData] = useState<UpdateProfileDto>() const [formData, setFormData] = useState<UpdateProfileDto>()
const [image, setImage] = useState<string | undefined>() const [image, setImage] = useState<string | undefined>()
const [countries, setCountries] = useState<CountryDto[]>([])
const auth = useStoreState((state) => state.auth) const auth = useStoreState((state) => state.auth)
const { setUser } = useStoreActions((actions) => actions.auth.user) const { setUser } = useStoreActions((actions) => actions.auth.user)
@ -54,8 +56,9 @@ const General = () => {
const fetchData = async () => { const fetchData = async () => {
setLoading(true) setLoading(true)
const response = await getProfile() const [response, countryResponse] = await Promise.all([getProfile(), getCountry()])
setProfileData(response.data) setProfileData(response.data)
setCountries(countryResponse.data)
setImage(auth.user.avatar) setImage(auth.user.avatar)
setFormData({ setFormData({
name: response.data.name, 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) { if (loading) {
return <></> return <></>
} }
@ -165,134 +231,173 @@ const General = () => {
}} }}
> >
{({ touched, errors, isSubmitting, resetForm }) => { {({ touched, errors, isSubmitting, resetForm }) => {
const validatorProps = { touched, errors }
return ( return (
<Form> <Form>
<FormContainer> <FormContainer size="md">
<FormDesription <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 w-full">
title={translate('::Abp.Identity.Profile.General')} <div>
desc={translate('::Abp.Identity.Profile.General.Description')} <h6 className="mb-4">
/> {translate('::Abp.Identity.User.UserInformation.ContactInformation')}
<FormRow </h6>
name="email"
label={translate('::Abp.Account.EmailAddress')} <FormItem label={translate('::Abp.Account.EmailAddress')}>
{...validatorProps} <Input
> type="text"
<Input disabled
type="text" prefix={<FaEnvelope className="text-xl" />}
disabled value={profileData?.email}
prefix={<FaEnvelope className="text-xl" />} ></Input>
value={profileData?.email} </FormItem>
></Input> <FormItem
</FormRow> label={translate('::Abp.Identity.User.UserInformation.Name')}
<FormRow invalid={(errors.name && touched.name) as boolean}
name="phoneNumber" errorMessage={errors.name}
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')} >
{...validatorProps} <Field
> type="text"
<Input autoComplete="off"
type="text" name="name"
disabled placeholder="Name"
prefix={<FaPhone className="text-xl" />} component={Input}
value={profileData?.phoneNumber} prefix={<FaUserCircle className="text-xl" />}
></Input> />
</FormRow> </FormItem>
<FormRow name="phoneNumber" label={translate('::RocketUsername')} {...validatorProps}> <FormItem
<Input label={translate('::Abp.Identity.User.UserInformation.Surname')}
type="text" invalid={(errors.surname && touched.surname) as boolean}
disabled errorMessage={errors.surname}
prefix={<FaFacebookMessenger className="text-xl" />} >
value={profileData?.extraProperties?.['RocketUsername'] as string | undefined} <Field
></Input> type="text"
</FormRow> autoComplete="off"
<FormRow name="surname"
name="name" placeholder="Last Name"
label={translate('::Abp.Identity.User.UserInformation.Name')} component={Input}
{...validatorProps} prefix={<FaUserCircle className="text-xl" />}
> />
<Field </FormItem>
type="text" <FormItem label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}>
autoComplete="off" <Input
name="name" type="text"
placeholder="Name" disabled
component={Input} prefix={<FaPhone className="text-xl" />}
prefix={<FaUserCircle className="text-xl" />} value={profileData?.phoneNumber}
/> ></Input>
</FormRow> </FormItem>
<FormRow <FormItem label={translate('::RocketUsername')}>
name="surname" <Input
label={translate('::Abp.Identity.User.UserInformation.Surname')} type="text"
{...validatorProps} disabled
> prefix={<FaFacebookMessenger className="text-xl" />}
<Field value={profileData?.extraProperties?.['RocketUsername'] as string | undefined}
type="text" ></Input>
autoComplete="off" </FormItem>
name="surname" </div>
placeholder="Last Name"
component={Input} <div>
prefix={<FaUserCircle className="text-xl" />} <h6 className="mb-4">
/> {translate('::Abp.Identity.User.UserInformation.AdditionalInformation')}
</FormRow> </h6>
<FormRow name="avatar" label="Avatar" alignCenter={false} {...validatorProps}>
<Field name="avatar"> <FormItem label={translate('::Abp.Account.HomeAddress')}>
{({ field, form }: FieldProps) => { <Input
return ( type="text"
<> disabled
<div> prefix={<FaHome className="text-xl" />}
<div className="flex flex-col lg:flex-row items-start gap-5"> value={getProfileExtraValue('HomeAddress')}
{image ? ( ></Input>
<Cropper </FormItem>
ref={cropperRef} <FormItem label={translate('::Abp.Account.Nationality')}>
src={image} <Select<SelectBoxOption>
onUpdate={(cropper) => { isDisabled
setTimeout(() => { options={nationalityOptions}
previewRef.current?.update(cropper) value={getSelectValue(
}, 100) nationalityOptions,
}} getProfileExtraValue('Nationality'),
className="cropper max-h-[300px] max-w-[300px]" )}
stencilComponent={CircleStencil} />
minHeight={100} </FormItem>
minWidth={100} <FormItem label={translate('::Abp.Account.EducationLevel')}>
/> <Select<SelectBoxOption>
) : ( isDisabled
<img options={educationOptions}
className="cropper max-h-[300px] max-w-[300px]" value={getSelectValue(
src={auth.user.avatar} educationOptions,
/> getProfileExtraValue('EducationLevel'),
)} )}
<div className="flex flex-row gap-4"> />
<div className="flex flex-col"> </FormItem>
<Upload <FormItem label={translate('::Abp.Account.GraduationSchool')}>
className="cursor-pointer" <Input
showList={false} type="text"
multiple={false} disabled
uploadLimit={1} prefix={<FaUniversity className="text-xl" />}
beforeUpload={beforeUpload} value={getProfileExtraValue('GraduationSchool')}
onChange={onChooseImage} ></Input>
> </FormItem>
<Button icon={<FaPlus />} type="button"></Button> <FormItem label={translate('::Abp.Account.BloodType')}>
</Upload> <Select<SelectBoxOption>
<Button isDisabled
type="button" options={bloodTypeOptions}
className="my-2" value={getSelectValue(bloodTypeOptions, getProfileExtraValue('BloodType'))}
icon={<FaTrashAlt />} />
onClick={() => setImage(undefined)} </FormItem>
></Button> </div>
</div>
{image && ( <div>
<CropperPreview <h6 className="mb-4">Avatar</h6>
ref={previewRef}
className="preview max-w-[100px] avatar-img avatar-circle border border-gray-400" <FormItem>
/> <div className="flex flex-col gap-4">
)} {image ? (
</div> <Cropper
</div> 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> </div>
</> {image && (
) <CropperPreview
}} ref={previewRef}
</Field> className="preview max-w-[100px] avatar-img avatar-circle"
</FormRow> />
)}
</div>
</div>
</FormItem>
</div>
</div>
<div className="mt-4 ltr:text-right"> <div className="mt-4 ltr:text-right">
<Button className="ltr:mr-2 rtl:ml-2" type="button" onClick={() => resetForm()}> <Button className="ltr:mr-2 rtl:ml-2" type="button" onClick={() => resetForm()}>
{translate('::Cancel')} {translate('::Cancel')}

View file

@ -9,6 +9,7 @@ import {
Select, Select,
Tabs, Tabs,
toast, toast,
Upload,
} from '@/components/ui' } from '@/components/ui'
import Dialog from '@/components/ui/Dialog' import Dialog from '@/components/ui/Dialog'
import DateTimepicker from '@/components/ui/DatePicker/DateTimepicker' import DateTimepicker from '@/components/ui/DatePicker/DateTimepicker'
@ -29,11 +30,12 @@ import {
putUserLookout, putUserLookout,
putUserPermission, putUserPermission,
} from '@/services/identity.service' } from '@/services/identity.service'
import { updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service' import { CountryDto, getCountry } from '@/services/home.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Field, FieldArray, FieldProps, Form, Formik, FormikHelpers } from 'formik' 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 { Helmet } from 'react-helmet'
import { import {
FaBuilding, FaBuilding,
@ -43,13 +45,42 @@ import {
FaTrashAlt, FaTrashAlt,
FaCheckCircle, FaCheckCircle,
FaUserAstronaut, 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' } 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 { useParams } from 'react-router-dom'
import * as Yup from 'yup' import * as Yup from 'yup'
import { SelectBoxOption, SelectBoxOptionWithGroup } from '@/types/shared' 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 { 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 { export interface ClaimTypeDto {
claimType: string claimType: string
@ -64,10 +95,18 @@ function UserDetails() {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [confirmDeleteClaim, setConfirmDeleteClaim] = useState<AssignedClaimViewModel | null>(null) const [confirmDeleteClaim, setConfirmDeleteClaim] = useState<AssignedClaimViewModel | null>(null)
const [countries, setCountries] = useState<CountryDto[]>([]) 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 || '') const { data } = await getUserDetail(userId || '')
setUserDetails(data) setUserDetails(data)
if (syncAvatar) {
setImage(`${AVATAR_URL(data.id, data.tenantId)}?${dayjs().unix()}`)
}
} }
useEffect(() => { useEffect(() => {
@ -80,6 +119,58 @@ function UserDetails() {
claimValue: Yup.string().required(), 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 ( const handleSubmit = async (
values: ClaimTypeDto, values: ClaimTypeDto,
{ setSubmitting }: FormikHelpers<ClaimTypeDto>, { setSubmitting }: FormikHelpers<ClaimTypeDto>,
@ -121,35 +212,60 @@ function UserDetails() {
title={userDetails.email} title={userDetails.email}
defaultTitle={APP_NAME} defaultTitle={APP_NAME}
></Helmet> ></Helmet>
<Container> <AdaptableCard>
<Tabs defaultValue="user"> <Tabs defaultValue="user">
<TabList> <TabList>
<TabNav value="user" icon={<FaUser />}> <TabNav value="user" icon={<FaUser className="text-sm" />}>
{translate('::Abp.Identity.User.UserInformation')} {translate('::Abp.Identity.User.UserInformation')}
</TabNav> </TabNav>
<TabNav value="permission" icon={<FaCheckCircle />}> <TabNav value="permission" icon={<FaCheckCircle className="text-sm" />}>
{translate('::Abp.Identity.User.Permissions')} {translate('::Abp.Identity.User.Permissions')}
</TabNav> </TabNav>
<TabNav value="work" icon={<FaBuilding />}> <TabNav value="work" icon={<FaBuilding className="text-sm" />}>
{translate('::Abp.Identity.User.WorkInformation')} {translate('::Abp.Identity.User.WorkInformation')}
</TabNav> </TabNav>
<TabNav value="identity" icon={<FaUserAstronaut />}> <TabNav value="identity" icon={<FaUserAstronaut className="text-sm" />}>
{translate('::Abp.Identity.User.IndentityInformation')} {translate('::Abp.Identity.User.IndentityInformation')}
</TabNav> </TabNav>
<TabNav value="lockout" icon={<FaLockOpen />}> <TabNav value="lockout" icon={<FaLockOpen className="text-sm" />}>
{translate('::Abp.Identity.User.LockoutManagement')} {translate('::Abp.Identity.User.LockoutManagement')}
</TabNav> </TabNav>
<TabNav value="claimTypes" icon={<FaFileAlt />}> <TabNav value="claimTypes" icon={<FaFileAlt className="text-sm" />}>
{translate('::Abp.Identity.User.ClaimTypes')} {translate('::Abp.Identity.User.ClaimTypes')}
</TabNav> </TabNav>
</TabList> </TabList>
<TabContent value="user"> <TabContent value="user">
<div className="mt-5"> <div className="px-4 py-6">
<Formik <Formik
initialValues={userDetails} initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => { onSubmit={async (values, { resetForm, setSubmitting }) => {
setSubmitting(true) setSubmitting(true)
await putUserDetail({ ...values }) 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( toast.push(
<Notification type="success" duration={2000}> <Notification type="success" duration={2000}>
@ -160,7 +276,7 @@ function UserDetails() {
}, },
) )
getUser() getUser(!keepCurrentAvatar)
setSubmitting(false) setSubmitting(false)
}} }}
> >
@ -169,7 +285,7 @@ function UserDetails() {
<Form> <Form>
<div> <div>
<FormContainer size="md"> <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 */} {/* Personal Information */}
<div> <div>
<h6 className="mb-4"> <h6 className="mb-4">
@ -177,6 +293,17 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.ContactInformation', '::Abp.Identity.User.UserInformation.ContactInformation',
)} )}
</h6> </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 <FormItem
label={translate('::Abp.Identity.User.UserInformation.Name')} label={translate('::Abp.Identity.User.UserInformation.Name')}
> >
@ -185,6 +312,7 @@ function UserDetails() {
name="name" name="name"
placeholder="Name" placeholder="Name"
component={Input} component={Input}
prefix={<FaUserCircle className="text-xl" />}
/> />
</FormItem> </FormItem>
@ -196,18 +324,10 @@ function UserDetails() {
name="surname" name="surname"
placeholder="Surname" placeholder="Surname"
component={Input} component={Input}
prefix={<FaUserCircle className="text-xl" />}
/> />
</FormItem> </FormItem>
<FormItem label={translate('::Abp.Account.EmailAddress')}>
<Field
type="text"
disabled
name="email"
placeholder="Email Address"
component={Input}
/>
</FormItem>
<FormItem <FormItem
label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')} label={translate('::Abp.Identity.User.UserInformation.PhoneNumber')}
> >
@ -217,27 +337,18 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.PhoneNumber', '::Abp.Identity.User.UserInformation.PhoneNumber',
)} )}
prefix={<FaPhone className="text-xl" />}
component={Input} component={Input}
/> />
</FormItem> </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')}> <FormItem size="sm" label={translate('::RocketUsername')}>
<Field <Field
type="text" type="text"
name="rocketUsername" name="rocketUsername"
placeholder={translate('::RocketUsername')} placeholder={translate('::RocketUsername')}
component={Input} component={Input}
prefix={<FaFacebookMessenger className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -248,6 +359,16 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.AdditionalInformation', '::Abp.Identity.User.UserInformation.AdditionalInformation',
)} )}
</h6> </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')}> <FormItem label={translate('::Abp.Account.Nationality')}>
<Field type="text" name="nationality"> <Field type="text" name="nationality">
{({ field, form }: FieldProps<SelectBoxOption>) => { {({ field, form }: FieldProps<SelectBoxOption>) => {
@ -330,12 +451,14 @@ function UserDetails() {
}} }}
</Field> </Field>
</FormItem> </FormItem>
<FormItem label={translate('::Abp.Account.GraduationSchool')}> <FormItem label={translate('::Abp.Account.GraduationSchool')}>
<Field <Field
type="text" type="text"
name="graduationSchool" name="graduationSchool"
placeholder={translate('::GraduationSchool')} placeholder={translate('::GraduationSchool')}
component={Input} component={Input}
prefix={<FaUniversity className="text-xl" />}
/> />
</FormItem> </FormItem>
<FormItem label={translate('::Abp.Account.BloodType')}> <FormItem label={translate('::Abp.Account.BloodType')}>
@ -371,76 +494,73 @@ function UserDetails() {
</div> </div>
<div> <div>
<h6 className="mb-4"> <h6 className="mb-4">Avatar</h6>
{translate('::Abp.Identity.User.UserInformation.AccountTimestamps')}
</h6> <FormItem>
<FormItem <div className="flex flex-col gap-4">
label={translate( {image ? (
'::Abp.Identity.User.UserInformation.PasswordChangeTime', <Cropper
)} ref={cropperRef}
> src={image}
<Field name="lastPasswordChangeTime"> onUpdate={(cropper) => {
{({ field, form }: FieldProps) => ( setTimeout(() => {
<DateTimepicker previewRef.current?.update(cropper)
inputFormat="DD/MM/YYYY HH:mm" }, 100)
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,
)
}} }}
className="cropper max-h-[300px] max-w-[300px]"
stencilComponent={CircleStencil}
minHeight={100}
minWidth={100}
/> />
)} ) : (
</Field> <img
</FormItem> className="cropper max-h-[300px] max-w-[300px]"
<FormItem src={
label={translate('::Abp.Identity.User.UserInformation.CreateTime')} image ||
> AVATAR_URL(
<Field name="creationTime"> values.id,
{({ field, form }: FieldProps) => ( values.tenantId,
<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">
</FormItem> <div className="flex flex-col gap-2">
<FormItem <Upload
label={translate('::Abp.Identity.User.UserInformation.UpdateTime')} className="cursor-pointer"
> showList={false}
<Field name="lastModificationTime"> multiple={false}
{({ field, form }: FieldProps) => ( uploadLimit={1}
<Input beforeUpload={beforeUpload}
field={field} onChange={onChooseImage}
form={form} >
value={ <Button icon={<FaPlus />} type="button"></Button>
field.value </Upload>
? dayjs(field.value).format('DD/MM/YYYY HH:mm') <Button
: undefined type="button"
} icon={<FaTrashAlt />}
disabled onClick={() => setImage(undefined)}
/> ></Button>
)} </div>
</Field> {image && (
<CropperPreview
ref={previewRef}
className="preview max-w-[100px] avatar-img avatar-circle"
/>
)}
</div>
</div>
</FormItem> </FormItem>
</div> </div>
</div> </div>
</FormContainer> </FormContainer>
</div> </div>
<Button variant="solid" block loading={isSubmitting} type="submit"> <div className="mt-4 ltr:text-right">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} <Button variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form> </Form>
) )
}} }}
@ -449,7 +569,7 @@ function UserDetails() {
</TabContent> </TabContent>
<TabContent value="permission"> <TabContent value="permission">
<div className="mt-5"> <div className="px-4 py-6">
<Formik <Formik
initialValues={userDetails} initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
@ -547,8 +667,8 @@ function UserDetails() {
</FormContainer> </FormContainer>
</div> </div>
<div className="mt-4"> <div className="mt-4 ltr:text-right">
<Button variant="solid" block loading={isSubmitting} type="submit"> <Button variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button> </Button>
</div> </div>
@ -560,7 +680,7 @@ function UserDetails() {
</TabContent> </TabContent>
<TabContent value="work"> <TabContent value="work">
<div className="mt-5"> <div className="px-4 py-6">
<Formik <Formik
initialValues={userDetails} initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => { onSubmit={async (values, { resetForm, setSubmitting }) => {
@ -601,7 +721,7 @@ function UserDetails() {
<Form> <Form>
<div> <div>
<FormContainer size="md"> <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> <div>
<FormItem <FormItem
label={translate( label={translate(
@ -666,6 +786,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SskNo', '::Abp.Identity.User.UserInformation.SskNo',
)} )}
component={Input} component={Input}
prefix={<FaIdCard className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -683,6 +804,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.HireDate', '::Abp.Identity.User.UserInformation.HireDate',
)} )}
inputPrefix={<FaBriefcase className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -710,6 +832,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.TerminationDate', '::Abp.Identity.User.UserInformation.TerminationDate',
)} )}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -725,9 +848,11 @@ function UserDetails() {
</FormContainer> </FormContainer>
</div> </div>
<Button variant="solid" block loading={isSubmitting} type="submit"> <div className="mt-4 ltr:text-right">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} <Button variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form> </Form>
) )
}} }}
@ -736,7 +861,7 @@ function UserDetails() {
</TabContent> </TabContent>
<TabContent value="identity"> <TabContent value="identity">
<div className="mt-5"> <div className="px-4 py-6">
<Formik <Formik
initialValues={userDetails} initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => { onSubmit={async (values, { resetForm, setSubmitting }) => {
@ -779,7 +904,7 @@ function UserDetails() {
return ( return (
<Form> <Form>
<FormContainer size="md"> <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> <div>
<FormItem <FormItem
label={translate( label={translate(
@ -793,6 +918,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.IdentityNumber', '::Abp.Identity.User.UserInformation.IdentityNumber',
)} )}
component={Input} component={Input}
prefix={<FaIdCard className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -808,6 +934,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SerialNo', '::Abp.Identity.User.UserInformation.SerialNo',
)} )}
component={Input} component={Input}
prefix={<FaHashtag className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -823,6 +950,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.Province', '::Abp.Identity.User.UserInformation.Province',
)} )}
component={Input} component={Input}
prefix={<FaMapMarkerAlt className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -838,6 +966,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.District', '::Abp.Identity.User.UserInformation.District',
)} )}
component={Input} component={Input}
prefix={<FaCity className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -853,6 +982,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.Village', '::Abp.Identity.User.UserInformation.Village',
)} )}
component={Input} component={Input}
prefix={<FaHome className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -868,6 +998,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.VolumeNo', '::Abp.Identity.User.UserInformation.VolumeNo',
)} )}
component={Input} component={Input}
prefix={<FaBook className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -885,6 +1016,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.FamilySequenceNo', '::Abp.Identity.User.UserInformation.FamilySequenceNo',
)} )}
component={Input} component={Input}
prefix={<FaUsers className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -900,6 +1032,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.SequenceNo', '::Abp.Identity.User.UserInformation.SequenceNo',
)} )}
component={Input} component={Input}
prefix={<FaHashtag className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -915,6 +1048,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.IssuedPlace', '::Abp.Identity.User.UserInformation.IssuedPlace',
)} )}
component={Input} component={Input}
prefix={<FaMapPin className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -932,6 +1066,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.IssuedDate', '::Abp.Identity.User.UserInformation.IssuedDate',
)} )}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -955,6 +1090,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.BirthPlace', '::Abp.Identity.User.UserInformation.BirthPlace',
)} )}
component={Input} component={Input}
prefix={<FaMapMarkerAlt className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -972,6 +1108,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.BirthDate', '::Abp.Identity.User.UserInformation.BirthDate',
)} )}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -995,6 +1132,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.FatherName', '::Abp.Identity.User.UserInformation.FatherName',
)} )}
component={Input} component={Input}
prefix={<FaUserTie className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -1010,6 +1148,7 @@ function UserDetails() {
'::Abp.Identity.User.UserInformation.MotherName', '::Abp.Identity.User.UserInformation.MotherName',
)} )}
component={Input} component={Input}
prefix={<FaFemale className="text-xl" />}
/> />
</FormItem> </FormItem>
</div> </div>
@ -1050,6 +1189,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.UserInformation.MarriageDate', '::Abp.Identity.User.UserInformation.MarriageDate',
)} )}
inputPrefix={<FaHeart className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -1064,9 +1204,11 @@ function UserDetails() {
</div> </div>
</FormContainer> </FormContainer>
<Button variant="solid" block loading={isSubmitting} type="submit"> <div className="mt-4 ltr:text-right">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} <Button variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form> </Form>
) )
}} }}
@ -1075,7 +1217,7 @@ function UserDetails() {
</TabContent> </TabContent>
<TabContent value="lockout"> <TabContent value="lockout">
<div className="mt-5"> <div className="px-4 py-6">
<Formik <Formik
initialValues={userDetails} initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => { onSubmit={async (values, { setSubmitting }) => {
@ -1108,7 +1250,7 @@ function UserDetails() {
return ( return (
<Form> <Form>
<FormContainer size="md"> <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 */} {/* Account Status */}
<div> <div>
<h6 className="mb-4"> <h6 className="mb-4">
@ -1258,6 +1400,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.LockoutManagement.AccountEndDate', '::Abp.Identity.User.LockoutManagement.AccountEndDate',
)} )}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -1295,7 +1438,12 @@ function UserDetails() {
'::Abp.Identity.User.LockoutManagement.AccessFailedCount', '::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> </FormItem>
</div> </div>
@ -1346,6 +1494,7 @@ function UserDetails() {
placeholder={translate( placeholder={translate(
'::Abp.Identity.User.LockoutManagement.LoginEndDate', '::Abp.Identity.User.LockoutManagement.LoginEndDate',
)} )}
inputPrefix={<FaCalendarAlt className="text-xl" />}
onChange={(date) => { onChange={(date) => {
form.setFieldValue( form.setFieldValue(
field.name, field.name,
@ -1356,13 +1505,88 @@ function UserDetails() {
)} )}
</Field> </Field>
</FormItem> </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>
</div> </div>
</FormContainer> </FormContainer>
<Button variant="solid" block loading={isSubmitting} type="submit"> <div className="mt-4 ltr:text-right">
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} <Button variant="solid" loading={isSubmitting} type="submit">
</Button> {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</Form> </Form>
) )
}} }}
@ -1371,7 +1595,7 @@ function UserDetails() {
</TabContent> </TabContent>
<TabContent value="claimTypes"> <TabContent value="claimTypes">
<div className="mt-5"> <div className="px-4 py-6">
<Table compact> <Table compact>
<THead> <THead>
<Tr> <Tr>
@ -1424,7 +1648,7 @@ function UserDetails() {
</div> </div>
</TabContent> </TabContent>
</Tabs> </Tabs>
</Container> </AdaptableCard>
<Dialog isOpen={open} onClose={() => setOpen(false)} onRequestClose={() => setOpen(false)}> <Dialog isOpen={open} onClose={() => setOpen(false)} onRequestClose={() => setOpen(false)}>
<Formik <Formik
@ -1465,7 +1689,13 @@ function UserDetails() {
invalid={errors.claimValue && touched.claimValue} invalid={errors.claimValue && touched.claimValue}
errorMessage={errors.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> </FormItem>
<div className="mt-6 flex flex-row justify-end gap-3"> <div className="mt-6 flex flex-row justify-end gap-3">