This commit is contained in:
@@ -0,0 +1,169 @@
|
|||||||
|
import { useEffect, useRef, useState, type FC, type MouseEvent } from 'react'
|
||||||
|
import { ArrowDown2, CloseCircle, TickSquare } from 'iconsax-react'
|
||||||
|
import { clx } from '../helpers/utils'
|
||||||
|
import type { ItemsSelectType } from './Select'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string
|
||||||
|
items: ItemsSelectType[]
|
||||||
|
value: string[]
|
||||||
|
onChange: (value: string[]) => void
|
||||||
|
onBlur?: () => void
|
||||||
|
error_text?: string
|
||||||
|
placeholder?: string
|
||||||
|
label?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelect: FC<Props> = ({
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
error_text,
|
||||||
|
placeholder = 'انتخاب کنید',
|
||||||
|
label,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const selectedValues = value ?? []
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsOpen(false)
|
||||||
|
onBlur?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}, [isOpen, onBlur])
|
||||||
|
|
||||||
|
const selectedItems = items.filter((item) =>
|
||||||
|
selectedValues.includes(String(item.value)),
|
||||||
|
)
|
||||||
|
|
||||||
|
const toggleValue = (itemValue: string) => {
|
||||||
|
if (selectedValues.includes(itemValue)) {
|
||||||
|
onChange(selectedValues.filter((v) => v !== itemValue))
|
||||||
|
} else {
|
||||||
|
onChange([...selectedValues, itemValue])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClear = (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange([])
|
||||||
|
onBlur?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayText =
|
||||||
|
selectedItems.length === 0
|
||||||
|
? placeholder
|
||||||
|
: selectedItems.length <= 2
|
||||||
|
? selectedItems.map((item) => item.label).join('، ')
|
||||||
|
: `${selectedItems.length} مورد انتخاب شده`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clx('w-full', className)} ref={wrapperRef}>
|
||||||
|
{label && (
|
||||||
|
<label className="text-sm text-primary-content">{label}</label>
|
||||||
|
)}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => setIsOpen((open) => !open)}
|
||||||
|
className={clx(
|
||||||
|
'w-full bg-white border border-border input-surface relative block appearance-none px-2.5 h-10 text-sm rounded-[10px] transition-colors text-right',
|
||||||
|
selectedItems.length === 0 && 'text-[#8c90a3]',
|
||||||
|
selectedItems.length > 0 && 'pl-10',
|
||||||
|
label && 'mt-1',
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="block truncate pr-6">{displayText}</span>
|
||||||
|
<ArrowDown2
|
||||||
|
size={16}
|
||||||
|
color="#8c90a3"
|
||||||
|
className="absolute z-0 top-3 left-2 pointer-events-none"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className="absolute top-0 bottom-0 my-auto left-8 z-10 flex items-center"
|
||||||
|
aria-label="پاک کردن"
|
||||||
|
>
|
||||||
|
<CloseCircle size={16} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="absolute z-20 top-full left-0 right-0 mt-1 bg-white border border-border rounded-[10px] shadow-lg max-h-60 overflow-auto">
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<div className="px-3 py-2.5 text-xs text-[#8c90a3]">
|
||||||
|
موردی برای انتخاب نیست
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="py-1">
|
||||||
|
{items.map((item) => {
|
||||||
|
const itemValue = String(item.value)
|
||||||
|
const isSelected = selectedValues.includes(itemValue)
|
||||||
|
return (
|
||||||
|
<li key={itemValue}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleValue(itemValue)}
|
||||||
|
className={clx(
|
||||||
|
'w-full text-right px-3 py-2 text-sm hover:bg-muted transition-colors flex items-center justify-between gap-2',
|
||||||
|
isSelected && 'bg-blue-50 text-[#0037FF]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{item.label}</span>
|
||||||
|
{isSelected && (
|
||||||
|
<TickSquare size={16} color="#0037FF" className="shrink-0" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedItems.length > 0 && (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{selectedItems.map((item) => (
|
||||||
|
<span
|
||||||
|
key={String(item.value)}
|
||||||
|
className="inline-flex items-center gap-1 rounded-lg border border-border bg-[#FBFCFF] px-2 py-1 text-xs text-[#1F2937]"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => toggleValue(String(item.value))}
|
||||||
|
aria-label={`حذف ${item.label}`}
|
||||||
|
className="text-[#8c90a3] hover:text-red-500"
|
||||||
|
>
|
||||||
|
<CloseCircle size={12} color="currentColor" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error_text ? (
|
||||||
|
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||||
|
{error_text}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MultiSelect
|
||||||
@@ -50,7 +50,7 @@ const UploadBox: FC<Props> = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='text-sm'>{props.label}</div>
|
<div className='text-sm'>{props.label}</div>
|
||||||
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
<div className='w-full h-10 mt-1 border border-border rounded-xl items-center px-2 flex gap-2.5'>
|
||||||
<button {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
<button type="button" {...getRootProps()} className=' w-fit h-7 rounded-lg text-xs px-6 bg-secondary'>
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
|
|
||||||
{t('uploadBox.selectFile')}
|
{t('uploadBox.selectFile')}
|
||||||
|
|||||||
+38
-12
@@ -1,12 +1,14 @@
|
|||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { type FC } from 'react';
|
import { useState, type FC } from 'react';
|
||||||
import type { CreateAdminType } from './types/Types';
|
import type { CreateAdminType } from './types/Types';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { AddSquare } from 'iconsax-react';
|
import { AddSquare } from 'iconsax-react';
|
||||||
import Input from '@/components/Input';
|
import Input from '@/components/Input';
|
||||||
import Select from '@/components/Select';
|
import Select from '@/components/Select';
|
||||||
|
import UploadBox from '@/components/UploadBox';
|
||||||
import { useCreateAdmin, useGetRoles } from './hooks/useAdminData';
|
import { useCreateAdmin, useGetRoles } from './hooks/useAdminData';
|
||||||
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { extractErrorMessage } from '@/config/func';
|
import { extractErrorMessage } from '@/config/func';
|
||||||
@@ -16,6 +18,20 @@ const CreateAdmin: FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { mutate: createAdmin, isPending } = useCreateAdmin();
|
const { mutate: createAdmin, isPending } = useCreateAdmin();
|
||||||
const { data: rolesData } = useGetRoles();
|
const { data: rolesData } = useGetRoles();
|
||||||
|
const { mutate: upload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
|
const handleSave = (values: CreateAdminType) => {
|
||||||
|
createAdmin(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('ادمین با موفقیت ایجاد شد');
|
||||||
|
navigate(Paths.admin.list);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik<CreateAdminType>({
|
const formik = useFormik<CreateAdminType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -23,6 +39,7 @@ const CreateAdmin: FC = () => {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
roleId: '',
|
roleId: '',
|
||||||
|
avatarUrl: undefined,
|
||||||
notificationPrefrences: ['test']
|
notificationPrefrences: ['test']
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
@@ -38,15 +55,17 @@ const CreateAdmin: FC = () => {
|
|||||||
roleId: Yup.string().required('نقش اجباری می باشد'),
|
roleId: Yup.string().required('نقش اجباری می باشد'),
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
createAdmin(values, {
|
if (file) {
|
||||||
onSuccess: () => {
|
upload(file, {
|
||||||
toast.success('ادمین با موفقیت ایجاد شد');
|
onSuccess: (data) => {
|
||||||
navigate(Paths.admin.list);
|
values.avatarUrl = data?.data?.key;
|
||||||
},
|
handleSave(values);
|
||||||
onError: (error) => {
|
},
|
||||||
toast.error(extractErrorMessage(error));
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
},
|
});
|
||||||
});
|
} else {
|
||||||
|
handleSave(values);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,7 +82,7 @@ const CreateAdmin: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={isPending}
|
isLoading={isPending || isUploading}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1.5'>
|
<div className='flex gap-1.5'>
|
||||||
<AddSquare size={18} color='black' />
|
<AddSquare size={18} color='black' />
|
||||||
@@ -73,7 +92,14 @@ const CreateAdmin: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 bg-white p-6 rounded-3xl flex-1'>
|
<div className='mt-8 bg-white p-6 rounded-3xl flex-1'>
|
||||||
<div className='rowTwoInput'>
|
<div>
|
||||||
|
<UploadBox
|
||||||
|
label='آواتار (اختیاری)'
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput mt-6'>
|
||||||
<Input
|
<Input
|
||||||
label='نام'
|
label='نام'
|
||||||
placeholder='نام را وارد کنید'
|
placeholder='نام را وارد کنید'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, type FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import { useGetAdmins, useDeleteAdmin } from "./hooks/useAdminData";
|
import { useGetAdmins, useDeleteAdmin } from "./hooks/useAdminData";
|
||||||
import Table from "@/components/Table";
|
import Table from "@/components/Table";
|
||||||
|
import PresignedImage from "@/components/PresignedImage";
|
||||||
import moment from "moment-jalaali";
|
import moment from "moment-jalaali";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Paths } from "@/config/Paths";
|
import { Paths } from "@/config/Paths";
|
||||||
@@ -29,6 +30,20 @@ const AdminList: FC = () => {
|
|||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={[
|
columns={[
|
||||||
|
{
|
||||||
|
key: "avatarUrl",
|
||||||
|
title: "آواتار",
|
||||||
|
render: (item) =>
|
||||||
|
item.avatarUrl ? (
|
||||||
|
<PresignedImage
|
||||||
|
src={item.avatarUrl}
|
||||||
|
className="w-10 h-10 rounded-full object-cover"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "fullName",
|
key: "fullName",
|
||||||
title: "نام و نام خانوادگی",
|
title: "نام و نام خانوادگی",
|
||||||
|
|||||||
+52
-14
@@ -1,12 +1,15 @@
|
|||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { type FC, useEffect } from 'react';
|
import { type FC, useEffect, useState } from 'react';
|
||||||
import type { CreateAdminType } from './types/Types';
|
import type { CreateAdminType } from './types/Types';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { Edit } from 'iconsax-react';
|
import { Edit } from 'iconsax-react';
|
||||||
import Input from '@/components/Input';
|
import Input from '@/components/Input';
|
||||||
import Select from '@/components/Select';
|
import Select from '@/components/Select';
|
||||||
|
import UploadBox from '@/components/UploadBox';
|
||||||
|
import PresignedImage from '@/components/PresignedImage';
|
||||||
import { useGetAdminById, useGetRoles, useUpdateAdmin } from './hooks/useAdminData';
|
import { useGetAdminById, useGetRoles, useUpdateAdmin } from './hooks/useAdminData';
|
||||||
|
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { extractErrorMessage } from '@/config/func';
|
import { extractErrorMessage } from '@/config/func';
|
||||||
@@ -18,6 +21,22 @@ const UpdateAdmin: FC = () => {
|
|||||||
const { mutate: update, isPending: isUpdating } = useUpdateAdmin();
|
const { mutate: update, isPending: isUpdating } = useUpdateAdmin();
|
||||||
const { data: rolesData } = useGetRoles();
|
const { data: rolesData } = useGetRoles();
|
||||||
const { data: adminData, isLoading: isLoadingAdmin } = useGetAdminById(adminId!);
|
const { data: adminData, isLoading: isLoadingAdmin } = useGetAdminById(adminId!);
|
||||||
|
const { mutate: upload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
|
const handleSave = (values: CreateAdminType) => {
|
||||||
|
if (!adminId) return;
|
||||||
|
|
||||||
|
update({ adminId: adminId, params: values }, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('ادمین با موفقیت ویرایش شد');
|
||||||
|
navigate(Paths.admin.list);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(extractErrorMessage(error));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik<CreateAdminType>({
|
const formik = useFormik<CreateAdminType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -25,6 +44,7 @@ const UpdateAdmin: FC = () => {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
roleId: '',
|
roleId: '',
|
||||||
|
avatarUrl: undefined,
|
||||||
notificationPrefrences: ['test']
|
notificationPrefrences: ['test']
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
@@ -40,17 +60,17 @@ const UpdateAdmin: FC = () => {
|
|||||||
roleId: Yup.string().required('نقش اجباری می باشد'),
|
roleId: Yup.string().required('نقش اجباری می باشد'),
|
||||||
}),
|
}),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
if (!adminId) return;
|
if (file) {
|
||||||
|
upload(file, {
|
||||||
update({ adminId: adminId, params: values }, {
|
onSuccess: (data) => {
|
||||||
onSuccess: () => {
|
values.avatarUrl = data?.data?.key;
|
||||||
toast.success('ادمین با موفقیت ویرایش شد');
|
handleSave(values);
|
||||||
navigate(Paths.admin.list);
|
},
|
||||||
},
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
onError: (error) => {
|
});
|
||||||
toast.error(extractErrorMessage(error));
|
} else {
|
||||||
},
|
handleSave(values);
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -62,6 +82,7 @@ const UpdateAdmin: FC = () => {
|
|||||||
lastName: adminData?.data.lastName || '',
|
lastName: adminData?.data.lastName || '',
|
||||||
phone: '0' + adminData?.data.phone || '',
|
phone: '0' + adminData?.data.phone || '',
|
||||||
roleId: adminData?.data.role?.id || '',
|
roleId: adminData?.data.role?.id || '',
|
||||||
|
avatarUrl: adminData?.data.avatarUrl || undefined,
|
||||||
notificationPrefrences: ['test']
|
notificationPrefrences: ['test']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -101,7 +122,7 @@ const UpdateAdmin: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className='w-fit px-6'
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={isUpdating}
|
isLoading={isUpdating || isUploading}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1.5'>
|
<div className='flex gap-1.5'>
|
||||||
<Edit size={18} color='black' />
|
<Edit size={18} color='black' />
|
||||||
@@ -111,7 +132,24 @@ const UpdateAdmin: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-8 bg-white p-6 rounded-3xl flex-1'>
|
<div className='mt-8 bg-white p-6 rounded-3xl flex-1'>
|
||||||
<div className='rowTwoInput'>
|
{formik.values.avatarUrl && !file && (
|
||||||
|
<div className='mb-4'>
|
||||||
|
<PresignedImage
|
||||||
|
src={formik.values.avatarUrl}
|
||||||
|
className='w-20 h-20 rounded-full object-cover'
|
||||||
|
alt='آواتار'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UploadBox
|
||||||
|
label='آواتار (اختیاری)'
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='rowTwoInput mt-6'>
|
||||||
<Input
|
<Input
|
||||||
label='نام'
|
label='نام'
|
||||||
placeholder='نام را وارد کنید'
|
placeholder='نام را وارد کنید'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface AdminItemType extends RowDataType {
|
|||||||
isSystem: boolean;
|
isSystem: boolean;
|
||||||
};
|
};
|
||||||
phone: string;
|
phone: string;
|
||||||
|
avatarUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminsType = BaseResponse<AdminItemType[]>;
|
export type AdminsType = BaseResponse<AdminItemType[]>;
|
||||||
@@ -25,6 +26,7 @@ export type CreateAdminType = {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
roleId: string;
|
roleId: string;
|
||||||
|
avatarUrl?: string;
|
||||||
notificationPrefrences?: string[];
|
notificationPrefrences?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import MultiSelect from '@/components/MultiSelect'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import BackButton from '@/components/BackButton'
|
import BackButton from '@/components/BackButton'
|
||||||
@@ -72,7 +73,7 @@ const getInitialValues = (invoiceItemId: string | null): ConvertToOrderItemType
|
|||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
attachments: [],
|
attachments: [],
|
||||||
designerId: '',
|
designerIds: [],
|
||||||
keepRequestChats: true,
|
keepRequestChats: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -85,7 +86,9 @@ const getValidationSchema = (invoiceItemId: string | null) =>
|
|||||||
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
||||||
title: Yup.string().required('عنوان اجباری است.'),
|
title: Yup.string().required('عنوان اجباری است.'),
|
||||||
description: Yup.string(),
|
description: Yup.string(),
|
||||||
designerId: Yup.string().required('این فیلد اجباری است'),
|
designerIds: Yup.array()
|
||||||
|
.of(Yup.string().required())
|
||||||
|
.min(1, 'انتخاب حداقل یک طراح اجباری است.'),
|
||||||
})
|
})
|
||||||
|
|
||||||
const ConvertToOrders: FC = () => {
|
const ConvertToOrders: FC = () => {
|
||||||
@@ -234,19 +237,23 @@ const ConvertToOrders: FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rowTwoInput">
|
<div className="rowTwoInput">
|
||||||
<Select
|
<MultiSelect
|
||||||
items={
|
items={
|
||||||
admins?.data?.map((item) => ({
|
admins?.data?.map((item) => ({
|
||||||
label: item.firstName + ' ' + item.lastName,
|
label: item.firstName + ' ' + item.lastName,
|
||||||
value: item.id,
|
value: item.id,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
label="طراح"
|
label="طراحان"
|
||||||
placeholder="انتخاب طراح"
|
placeholder="انتخاب طراحان"
|
||||||
{...formik.getFieldProps('designerId')}
|
value={formik.values.designerIds}
|
||||||
|
onChange={(designerIds) =>
|
||||||
|
formik.setFieldValue('designerIds', designerIds)
|
||||||
|
}
|
||||||
|
onBlur={() => formik.setFieldTouched('designerIds', true)}
|
||||||
error_text={
|
error_text={
|
||||||
formik.touched.designerId && formik.errors.designerId
|
formik.touched.designerIds && formik.errors.designerIds
|
||||||
? formik.errors.designerId
|
? String(formik.errors.designerIds)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useFormik } from 'formik'
|
|||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import Input from '@/components/Input'
|
import Input from '@/components/Input'
|
||||||
import Select from '@/components/Select'
|
import Select from '@/components/Select'
|
||||||
|
import MultiSelect from '@/components/MultiSelect'
|
||||||
import Textarea from '@/components/Textarea'
|
import Textarea from '@/components/Textarea'
|
||||||
import UploadBox from '@/components/UploadBox'
|
import UploadBox from '@/components/UploadBox'
|
||||||
import { useGetUsers } from '../user/hooks/useUserData'
|
import { useGetUsers } from '../user/hooks/useUserData'
|
||||||
@@ -53,7 +54,7 @@ const EditOrder: FC = () => {
|
|||||||
title: '',
|
title: '',
|
||||||
attachments: [],
|
attachments: [],
|
||||||
description: '',
|
description: '',
|
||||||
designerId: '',
|
designerIds: [],
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
userId: Yup.string().required('انتخاب کاربر اجباری است.'),
|
userId: Yup.string().required('انتخاب کاربر اجباری است.'),
|
||||||
@@ -61,7 +62,9 @@ const EditOrder: FC = () => {
|
|||||||
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
||||||
productId: Yup.string().required('انتخاب محصول اجباری است.'),
|
productId: Yup.string().required('انتخاب محصول اجباری است.'),
|
||||||
title: Yup.string().required('عنوان اجباری است.'),
|
title: Yup.string().required('عنوان اجباری است.'),
|
||||||
designerId: Yup.string().required('انتخاب طراح اجباری است.'),
|
designerIds: Yup.array()
|
||||||
|
.of(Yup.string().required())
|
||||||
|
.min(1, 'انتخاب حداقل یک طراح اجباری است.'),
|
||||||
}),
|
}),
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
@@ -96,14 +99,18 @@ const EditOrder: FC = () => {
|
|||||||
if (order) {
|
if (order) {
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
userId: order.user?.id ?? '',
|
userId: order.user?.id ?? '',
|
||||||
invoiceItemId: order.invoiceItem ?? '',
|
invoiceItemId: typeof order.invoiceItem === 'object' && order.invoiceItem
|
||||||
|
? order.invoiceItem.id
|
||||||
|
: '',
|
||||||
typeId: order.type?.id ?? '',
|
typeId: order.type?.id ?? '',
|
||||||
estimatedDays: order.estimatedDays ?? 0,
|
estimatedDays: order.estimatedDays ?? 0,
|
||||||
productId: order.product?.id ?? '',
|
productId: order.product?.id ?? '',
|
||||||
title: order.title ?? '',
|
title: order.title ?? '',
|
||||||
attachments: normalizeAttachments(order.attachments ?? []),
|
attachments: normalizeAttachments(order.attachments ?? []),
|
||||||
description: (order as OrderDetailDataType & { description?: string })?.description ?? '',
|
description: order?.description ?? '',
|
||||||
designerId: order.designer?.id ?? '',
|
designerIds: (order.designers ?? [])
|
||||||
|
.map((item) => item.designer?.id)
|
||||||
|
.filter(Boolean),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -121,12 +128,14 @@ const EditOrder: FC = () => {
|
|||||||
value: item.id,
|
value: item.id,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
|
|
||||||
const currentDesigner = order?.designer
|
for (const orderDesigner of order?.designers ?? []) {
|
||||||
if (currentDesigner?.id && !items.some((item) => item.value === currentDesigner.id)) {
|
const currentDesigner = orderDesigner.designer
|
||||||
items.unshift({
|
if (currentDesigner?.id && !items.some((item) => item.value === currentDesigner.id)) {
|
||||||
label: `${currentDesigner.firstName ?? ''} ${currentDesigner.lastName ?? ''}`.trim() || currentDesigner.phone,
|
items.unshift({
|
||||||
value: currentDesigner.id,
|
label: `${currentDesigner.firstName ?? ''} ${currentDesigner.lastName ?? ''}`.trim() || currentDesigner.phone,
|
||||||
})
|
value: currentDesigner.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -188,14 +197,16 @@ const EditOrder: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='rowTwoInput mt-6'>
|
<div className='rowTwoInput mt-6'>
|
||||||
<Select
|
<MultiSelect
|
||||||
items={designerSelectItems}
|
items={designerSelectItems}
|
||||||
label='طراح'
|
label='طراحان'
|
||||||
placeholder='انتخاب طراح'
|
placeholder='انتخاب طراحان'
|
||||||
{...formik.getFieldProps('designerId')}
|
value={formik.values.designerIds}
|
||||||
|
onChange={(designerIds) => formik.setFieldValue('designerIds', designerIds)}
|
||||||
|
onBlur={() => formik.setFieldTouched('designerIds', true)}
|
||||||
error_text={
|
error_text={
|
||||||
formik.touched.designerId && formik.errors.designerId
|
formik.touched.designerIds && formik.errors.designerIds
|
||||||
? formik.errors.designerId
|
? String(formik.errors.designerIds)
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type FC, useState } from 'react'
|
import { type FC, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Call, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
|
import { Call, DocumentDownload, Edit2, Location, Paperclip2, Profile2User, Receipt21, User } from 'iconsax-react'
|
||||||
import { clx } from '@/helpers/utils'
|
import { clx } from '@/helpers/utils'
|
||||||
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
import { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||||
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
import { getPresignedUrl } from '@/pages/uploader/service/UploaderService'
|
||||||
@@ -156,6 +156,15 @@ const OrderDetailSidebar: FC<Props> = ({
|
|||||||
<InfoRow label="نوع / دستهبندی" value={order?.type?.title} />
|
<InfoRow label="نوع / دستهبندی" value={order?.type?.title} />
|
||||||
<InfoRow label="محصول" value={order?.product?.title} />
|
<InfoRow label="محصول" value={order?.product?.title} />
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
{order?.description && (
|
||||||
|
<div className="mt-4 rounded-xl border border-dashed border-gray-200 bg-gray-50/80 px-4 py-3">
|
||||||
|
<p className="text-xs font-medium text-gray-500 mb-1.5">شرح سفارش</p>
|
||||||
|
<p className="text-sm leading-7 text-gray-800 whitespace-pre-wrap">
|
||||||
|
{order.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{order?.user && (
|
{order?.user && (
|
||||||
@@ -175,10 +184,10 @@ const OrderDetailSidebar: FC<Props> = ({
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(order?.creator || order?.designer) && (
|
{(order?.creator || (order?.designers?.length ?? 0) > 0) && (
|
||||||
<section className="bg-white rounded-3xl p-5 shadow-sm space-y-3">
|
<section className="bg-white rounded-3xl p-5 shadow-sm space-y-3">
|
||||||
<h3 className="text-sm font-medium text-gray-900">تیم</h3>
|
<h3 className="text-sm font-medium text-gray-900">تیم</h3>
|
||||||
{order.creator && (
|
{order?.creator && (
|
||||||
<PersonCard
|
<PersonCard
|
||||||
title="ایجادکننده"
|
title="ایجادکننده"
|
||||||
name={getCreatorDisplayName(order.creator)}
|
name={getCreatorDisplayName(order.creator)}
|
||||||
@@ -186,15 +195,16 @@ const OrderDetailSidebar: FC<Props> = ({
|
|||||||
phone={order.creator.phone}
|
phone={order.creator.phone}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{order.designer && (
|
{(order?.designers ?? []).map((orderDesigner) => (
|
||||||
<PersonCard
|
<PersonCard
|
||||||
|
key={orderDesigner.id}
|
||||||
title="طراح / مجری"
|
title="طراح / مجری"
|
||||||
name={getCreatorDisplayName(order.designer)}
|
name={getCreatorDisplayName(orderDesigner.designer)}
|
||||||
subtitle="مسئول طراحی"
|
subtitle="مسئول طراحی"
|
||||||
phone={order.designer.phone}
|
phone={orderDesigner.designer?.phone}
|
||||||
icon="designer"
|
icon="designer"
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -204,19 +214,29 @@ const OrderDetailSidebar: FC<Props> = ({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{attachments.map((key, idx) => {
|
{attachments.map((key, idx) => {
|
||||||
const { fileName } = getFileNameAndExtensionFromUrl(key)
|
const { fileName } = getFileNameAndExtensionFromUrl(key)
|
||||||
|
const handleDownload = async () => {
|
||||||
|
const resolvedUrl = await getPresignedUrl(key)
|
||||||
|
window.open(resolvedUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5"
|
||||||
onClick={async () => {
|
|
||||||
const resolvedUrl = await getPresignedUrl(key)
|
|
||||||
window.open(resolvedUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5 text-sm text-[#0037FF] hover:bg-blue-50 transition-colors text-right w-full"
|
|
||||||
>
|
>
|
||||||
<Paperclip2 size={18} className="shrink-0" />
|
<Paperclip2 size={18} className="shrink-0 text-gray-400" />
|
||||||
<span className="truncate">{fileName || `پیوست ${idx + 1}`}</span>
|
<span className="min-w-0 flex-1 truncate text-sm text-gray-900 text-right">
|
||||||
</button>
|
{fileName || `پیوست ${idx + 1}`}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title="دانلود"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-blue-200 bg-white px-2.5 py-1.5 text-xs font-medium text-[#0037FF] hover:bg-blue-50 transition-colors"
|
||||||
|
>
|
||||||
|
<DocumentDownload size={16} color="#0037FF" />
|
||||||
|
دانلود
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,14 +16,20 @@ const getCustomerName = (item: OrderListItemType) => {
|
|||||||
return fullName || user.phone || "—";
|
return fullName || user.phone || "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDesignerName = (item: OrderListItemType) => {
|
const getDesignerNames = (item: OrderListItemType) => {
|
||||||
const designer = item.designer;
|
const designers = item.designers ?? [];
|
||||||
if (!designer || typeof designer === "string") {
|
if (!designers.length) return "—";
|
||||||
return typeof designer === "string" ? designer : "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = [designer.firstName, designer.lastName].filter(Boolean).join(" ");
|
const names = designers
|
||||||
return name || designer.phone || "—";
|
.map((orderDesigner) => {
|
||||||
|
const designer = orderDesigner.designer;
|
||||||
|
if (!designer) return null;
|
||||||
|
const name = [designer.firstName, designer.lastName].filter(Boolean).join(" ");
|
||||||
|
return name || designer.phone || null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return names.length ? names.join("، ") : "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
type OrderListColumnsOptions = {
|
type OrderListColumnsOptions = {
|
||||||
@@ -59,9 +65,9 @@ export const getOrderListColumns = ({
|
|||||||
render: (item) => <span>{item.product?.title ?? "—"}</span>,
|
render: (item) => <span>{item.product?.title ?? "—"}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "designer",
|
key: "designers",
|
||||||
title: "مجری / طراح",
|
title: "مجری / طراح",
|
||||||
render: (item) => <span>{getDesignerName(item)}</span>,
|
render: (item) => <span>{getDesignerNames(item)}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "status",
|
key: "status",
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export interface OrderListItemType extends RowDataType {
|
|||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
user: UserType;
|
user: UserType;
|
||||||
designer: OrderDetailCreatorType | string | null;
|
designers: OrderDesignerType[];
|
||||||
creator: string;
|
creator: string;
|
||||||
orderNumber: number;
|
orderNumber: number;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -132,6 +132,13 @@ export type OrderDetailCreatorType = {
|
|||||||
phone: string;
|
phone: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OrderDesignerType = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
designer: OrderDetailCreatorType;
|
||||||
|
};
|
||||||
|
|
||||||
export type OrderDetailDataType = {
|
export type OrderDetailDataType = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -142,11 +149,12 @@ export type OrderDetailDataType = {
|
|||||||
title: string;
|
title: string;
|
||||||
type: OrderDetailCategoryType;
|
type: OrderDetailCategoryType;
|
||||||
user: UserType;
|
user: UserType;
|
||||||
designer: OrderDetailCreatorType | null;
|
designers: OrderDesignerType[];
|
||||||
creator: OrderDetailCreatorType;
|
creator: OrderDetailCreatorType;
|
||||||
orderNumber: number;
|
orderNumber: number;
|
||||||
status: string;
|
status: string;
|
||||||
estimatedDays: number;
|
estimatedDays: number;
|
||||||
|
description?: string | null;
|
||||||
attachments: unknown[];
|
attachments: unknown[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -251,7 +259,7 @@ export type UpdateOrderType = {
|
|||||||
title: string;
|
title: string;
|
||||||
attachments: string[];
|
attachments: string[];
|
||||||
description: string;
|
description: string;
|
||||||
designerId: string;
|
designerIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConvertToOrderItemType = {
|
export type ConvertToOrderItemType = {
|
||||||
@@ -263,7 +271,7 @@ export type ConvertToOrderItemType = {
|
|||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
attachments: string[];
|
attachments: string[];
|
||||||
designerId: string;
|
designerIds: string[];
|
||||||
keepRequestChats: boolean;
|
keepRequestChats: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+36
-10
@@ -1,12 +1,14 @@
|
|||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { type FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import { TickCircle } from "iconsax-react";
|
import { TickCircle } from "iconsax-react";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import Textarea from "@/components/Textarea";
|
import Textarea from "@/components/Textarea";
|
||||||
import SwitchComponent from "@/components/Switch";
|
import SwitchComponent from "@/components/Switch";
|
||||||
|
import UploadBox from "@/components/UploadBox";
|
||||||
import { useCreateUser } from "./hooks/useUserData";
|
import { useCreateUser } from "./hooks/useUserData";
|
||||||
|
import { useSingleUpload } from "@/pages/uploader/hooks/useUploader";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { extractErrorMessage } from "@/config/func";
|
import { extractErrorMessage } from "@/config/func";
|
||||||
@@ -20,6 +22,7 @@ const initialValues: CreateUserType = {
|
|||||||
address: "",
|
address: "",
|
||||||
gender: true,
|
gender: true,
|
||||||
maxCredit: 0,
|
maxCredit: 0,
|
||||||
|
avatarUrl: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
@@ -35,18 +38,34 @@ const validationSchema = Yup.object({
|
|||||||
const CreateUser: FC = () => {
|
const CreateUser: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { mutate: createUser, isPending } = useCreateUser();
|
const { mutate: createUser, isPending } = useCreateUser();
|
||||||
|
const { mutate: upload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
|
const handleSave = (values: CreateUserType) => {
|
||||||
|
createUser(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("مشتری با موفقیت ایجاد شد");
|
||||||
|
navigate(Paths.users.list);
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik<CreateUserType>({
|
const formik = useFormik<CreateUserType>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
createUser(values, {
|
if (file) {
|
||||||
onSuccess: () => {
|
upload(file, {
|
||||||
toast.success("مشتری با موفقیت ایجاد شد");
|
onSuccess: (data) => {
|
||||||
navigate(Paths.users.list);
|
values.avatarUrl = data?.data?.key;
|
||||||
},
|
handleSave(values);
|
||||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
},
|
||||||
});
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleSave(values);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,7 +76,7 @@ const CreateUser: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className="w-fit px-6"
|
className="w-fit px-6"
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={isPending}
|
isLoading={isPending || isUploading}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<TickCircle size={18} color="black" />
|
<TickCircle size={18} color="black" />
|
||||||
@@ -67,7 +86,14 @@ const CreateUser: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
|
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
|
||||||
<div className="rowTwoInput">
|
<div>
|
||||||
|
<UploadBox
|
||||||
|
label="آواتار (اختیاری)"
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rowTwoInput mt-6">
|
||||||
<Input
|
<Input
|
||||||
label="موبایل"
|
label="موبایل"
|
||||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, type FC } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useGetUsers } from './hooks/useUserData'
|
import { useGetUsers } from './hooks/useUserData'
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
|
import PresignedImage from '@/components/PresignedImage'
|
||||||
import moment from 'moment-jalaali'
|
import moment from 'moment-jalaali'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import { AddSquare, Edit } from 'iconsax-react'
|
import { AddSquare, Edit } from 'iconsax-react'
|
||||||
@@ -27,6 +28,13 @@ const UsersList: FC = () => {
|
|||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={[
|
columns={[
|
||||||
|
{
|
||||||
|
key: 'avatarUrl',
|
||||||
|
title: 'آواتار',
|
||||||
|
render: (item) => item.avatarUrl
|
||||||
|
? <PresignedImage src={item.avatarUrl} className='w-10 h-10 rounded-full object-cover' alt='' />
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'fullName',
|
key: 'fullName',
|
||||||
title: 'نام و نام خانوادگی',
|
title: 'نام و نام خانوادگی',
|
||||||
|
|||||||
+50
-12
@@ -1,15 +1,18 @@
|
|||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { type FC, useEffect } from "react";
|
import { type FC, useEffect, useState } from "react";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import { TickCircle } from "iconsax-react";
|
import { TickCircle } from "iconsax-react";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import Textarea from "@/components/Textarea";
|
import Textarea from "@/components/Textarea";
|
||||||
import SwitchComponent from "@/components/Switch";
|
import SwitchComponent from "@/components/Switch";
|
||||||
|
import UploadBox from "@/components/UploadBox";
|
||||||
|
import PresignedImage from "@/components/PresignedImage";
|
||||||
import {
|
import {
|
||||||
useGetUserById,
|
useGetUserById,
|
||||||
useUpdateUser,
|
useUpdateUser,
|
||||||
} from "./hooks/useUserData";
|
} from "./hooks/useUserData";
|
||||||
|
import { useSingleUpload } from "@/pages/uploader/hooks/useUploader";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { extractErrorMessage } from "@/config/func";
|
import { extractErrorMessage } from "@/config/func";
|
||||||
@@ -23,6 +26,7 @@ const initialValues: CreateUserType = {
|
|||||||
address: "",
|
address: "",
|
||||||
gender: true,
|
gender: true,
|
||||||
maxCredit: 0,
|
maxCredit: 0,
|
||||||
|
avatarUrl: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
@@ -42,22 +46,38 @@ const UpdateUser: FC = () => {
|
|||||||
!!userId
|
!!userId
|
||||||
);
|
);
|
||||||
const { mutate: updateUser, isPending } = useUpdateUser();
|
const { mutate: updateUser, isPending } = useUpdateUser();
|
||||||
|
const { mutate: upload, isPending: isUploading } = useSingleUpload();
|
||||||
|
const [file, setFile] = useState<File>();
|
||||||
|
|
||||||
|
const handleSave = (values: CreateUserType) => {
|
||||||
|
if (!userId) return;
|
||||||
|
updateUser(
|
||||||
|
{ userId, params: values },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("مشتری با موفقیت ویرایش شد");
|
||||||
|
navigate(Paths.users.list);
|
||||||
|
},
|
||||||
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const formik = useFormik<CreateUserType>({
|
const formik = useFormik<CreateUserType>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
if (!userId) return;
|
if (file) {
|
||||||
updateUser(
|
upload(file, {
|
||||||
{ userId, params: values },
|
onSuccess: (data) => {
|
||||||
{
|
values.avatarUrl = data?.data?.key;
|
||||||
onSuccess: () => {
|
handleSave(values);
|
||||||
toast.success("مشتری با موفقیت ویرایش شد");
|
|
||||||
navigate(Paths.users.list);
|
|
||||||
},
|
},
|
||||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||||
}
|
});
|
||||||
);
|
} else {
|
||||||
|
handleSave(values);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,6 +91,7 @@ const UpdateUser: FC = () => {
|
|||||||
address: u.addresse ?? "",
|
address: u.addresse ?? "",
|
||||||
gender: u.gender ?? true,
|
gender: u.gender ?? true,
|
||||||
maxCredit: u.maxCredit ?? 0,
|
maxCredit: u.maxCredit ?? 0,
|
||||||
|
avatarUrl: u.avatarUrl ?? undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -103,7 +124,7 @@ const UpdateUser: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className="w-fit px-6"
|
className="w-fit px-6"
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
isLoading={isPending}
|
isLoading={isPending || isUploading}
|
||||||
>
|
>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<TickCircle size={18} color="black" />
|
<TickCircle size={18} color="black" />
|
||||||
@@ -113,7 +134,24 @@ const UpdateUser: FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
|
<div className="mt-8 bg-white p-6 rounded-3xl flex-1">
|
||||||
<div className="rowTwoInput">
|
{formik.values.avatarUrl && !file && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<PresignedImage
|
||||||
|
src={formik.values.avatarUrl}
|
||||||
|
className="w-20 h-20 rounded-full object-cover"
|
||||||
|
alt="آواتار"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UploadBox
|
||||||
|
label="آواتار (اختیاری)"
|
||||||
|
onChange={(files) => setFile(files[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rowTwoInput mt-6">
|
||||||
<Input
|
<Input
|
||||||
label="موبایل"
|
label="موبایل"
|
||||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface UserType extends RowDataType {
|
|||||||
addresse: string | null;
|
addresse: string | null;
|
||||||
maxCredit: number;
|
maxCredit: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
avatarUrl: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
}
|
}
|
||||||
@@ -23,6 +24,7 @@ export interface CreateUserType {
|
|||||||
address: string;
|
address: string;
|
||||||
gender: boolean;
|
gender: boolean;
|
||||||
maxCredit: number;
|
maxCredit: number;
|
||||||
|
avatarUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserDetailResponseType = BaseResponse<UserType>;
|
export type UserDetailResponseType = BaseResponse<UserType>;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const SideBar: FC = () => {
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
'fixed right-0 top-0 bottom-0 flex w-[var(--layout-sidebar-width)] translate-x-[400px] flex-col overflow-hidden bg-white py-12 opacity-0 invisible transition-all ease-in-out xl:right-[var(--layout-frame)] xl:top-[var(--layout-frame)] xl:bottom-[var(--layout-frame)] xl:z-40 xl:translate-x-0 xl:rounded-[32px] xl:opacity-100 xl:visible',
|
'fixed right-0 top-0 bottom-0 flex w-[var(--layout-sidebar-width)] translate-x-[400px] flex-col overflow-hidden bg-white py-5 opacity-0 invisible transition-all ease-in-out xl:right-[var(--layout-frame)] xl:top-[var(--layout-frame)] xl:bottom-[var(--layout-frame)] xl:z-40 xl:translate-x-0 xl:rounded-[32px] xl:opacity-100 xl:visible',
|
||||||
openSidebar && 'visible z-40 flex translate-x-0 opacity-100'
|
openSidebar && 'visible z-40 flex translate-x-0 opacity-100'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user