sozsoft-platform/ui/src/views/admin/profile/components/General.tsx
Sedat ÖZTÜRK 04e88fc969 bottom-end
2026-07-08 09:14:40 +03:00

430 lines
14 KiB
TypeScript

import { Input, Select, Upload } from '@/components/ui'
import Button from '@/components/ui/Button'
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 { Field, Form, Formik } from 'formik'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
CircleStencil,
Cropper,
CropperPreview,
CropperPreviewRef,
CropperRef,
} from 'react-advanced-cropper'
import 'react-advanced-cropper/dist/style.css'
import {
FaFacebookMessenger,
FaEnvelope,
FaTrashAlt,
FaUserCircle,
FaPhone,
FaPlus,
FaHome,
FaUniversity,
} from 'react-icons/fa'
import * as Yup from 'yup'
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'
import { useSetting } from '@/utils/hooks/useSetting'
const schema = Yup.object().shape({
name: Yup.string().min(3).max(50).required(),
surname: Yup.string().min(3).max(50).required(),
})
const General = () => {
const [loading, setLoading] = useState(true)
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)
const { setting } = useSetting()
const isEmailUpdateEnabled = setting('Abp.Identity.User.IsEmailUpdateEnabled')
const { translate } = useLocalization()
const cropperRef = useRef<CropperRef>(null)
const previewRef = useRef<CropperPreviewRef>(null)
const fetchData = useCallback(async () => {
setLoading(true)
const [response, countryResponse] = await Promise.all([getProfile(), getCountry()])
setProfileData(response.data)
setCountries(countryResponse.data)
setImage(auth.user.avatar)
setFormData({
name: response.data.name,
surname: response.data.surname,
})
setLoading(false)
}, [auth.user.avatar])
useEffect(() => {
fetchData()
}, [fetchData])
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 onFormSubmit = async (
values: UpdateProfileDto,
setSubmitting: (isSubmitting: boolean) => void,
) => {
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) {
setUser({
...auth.user,
name: `${resp.data.name} ${resp.data.surname}`.trim(),
avatar: AVATAR_URL(auth.user.id, auth.tenant.tenantId) + `?${dayjs().unix()}`,
})
toast.push(<Notification title={'Profil güncellendi'} type="success" />, {
placement: 'bottom-end',
})
} else {
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
placement: 'bottom-end',
})
}
setSubmitting(false)
}
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 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 <></>
}
return (
<Formik
enableReinitialize
initialValues={formData!}
validationSchema={schema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true)
onFormSubmit(values, setSubmitting)
}}
>
{({ touched, errors, isSubmitting, resetForm }) => {
return (
<Form>
<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={isEmailUpdateEnabled?.toLowerCase() !== 'true'}
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
disabled
type="text"
prefix={<FaPhone className="text-xl" />}
value={profileData?.phoneNumber}
></Input>
</FormItem>
<FormItem label={translate('::RocketUsername')}>
<Input
disabled
type="text"
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
disabled
type="text"
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
disabled
type="text"
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}
className="cropper max-h-[300px] max-w-[300px]"
stencilComponent={CircleStencil}
minHeight={100}
minWidth={100}
onUpdate={(cropper) => {
setTimeout(() => {
previewRef.current?.update(cropper)
}, 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
size="sm"
variant="default"
icon={<FaPlus />}
type="button"
></Button>
</Upload>
<Button
size="sm"
variant="solid"
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>
<div className="mt-4 ltr:text-right">
<Button
size="sm"
className="ltr:mr-2 rtl:ml-2"
type="button"
onClick={() => resetForm()}
>
{translate('::Cancel')}
</Button>
<Button size="sm" variant="solid" loading={isSubmitting} type="submit">
{isSubmitting ? translate('::Saving') : translate('::Save')}
</Button>
</div>
</FormContainer>
</Form>
)
}}
</Formik>
)
}
export default General