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 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'>
|
||||
<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()} />
|
||||
|
||||
{t('uploadBox.selectFile')}
|
||||
|
||||
+38
-12
@@ -1,12 +1,14 @@
|
||||
import { useFormik } from 'formik';
|
||||
import { type FC } from 'react';
|
||||
import { useState, type FC } from 'react';
|
||||
import type { CreateAdminType } from './types/Types';
|
||||
import * as Yup from 'yup';
|
||||
import Button from '@/components/Button';
|
||||
import { AddSquare } from 'iconsax-react';
|
||||
import Input from '@/components/Input';
|
||||
import Select from '@/components/Select';
|
||||
import UploadBox from '@/components/UploadBox';
|
||||
import { useCreateAdmin, useGetRoles } from './hooks/useAdminData';
|
||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { extractErrorMessage } from '@/config/func';
|
||||
@@ -16,6 +18,20 @@ const CreateAdmin: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { mutate: createAdmin, isPending } = useCreateAdmin();
|
||||
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>({
|
||||
initialValues: {
|
||||
@@ -23,6 +39,7 @@ const CreateAdmin: FC = () => {
|
||||
lastName: '',
|
||||
phone: '',
|
||||
roleId: '',
|
||||
avatarUrl: undefined,
|
||||
notificationPrefrences: ['test']
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
@@ -38,15 +55,17 @@ const CreateAdmin: FC = () => {
|
||||
roleId: Yup.string().required('نقش اجباری می باشد'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
createAdmin(values, {
|
||||
onSuccess: () => {
|
||||
toast.success('ادمین با موفقیت ایجاد شد');
|
||||
navigate(Paths.admin.list);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error));
|
||||
},
|
||||
});
|
||||
if (file) {
|
||||
upload(file, {
|
||||
onSuccess: (data) => {
|
||||
values.avatarUrl = data?.data?.key;
|
||||
handleSave(values);
|
||||
},
|
||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||
});
|
||||
} else {
|
||||
handleSave(values);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,7 +82,7 @@ const CreateAdmin: FC = () => {
|
||||
<Button
|
||||
className='w-fit px-6'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isPending}
|
||||
isLoading={isPending || isUploading}
|
||||
>
|
||||
<div className='flex gap-1.5'>
|
||||
<AddSquare size={18} color='black' />
|
||||
@@ -73,7 +92,14 @@ const CreateAdmin: FC = () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
label='نام'
|
||||
placeholder='نام را وارد کنید'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, type FC } from "react";
|
||||
import { useGetAdmins, useDeleteAdmin } from "./hooks/useAdminData";
|
||||
import Table from "@/components/Table";
|
||||
import PresignedImage from "@/components/PresignedImage";
|
||||
import moment from "moment-jalaali";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Paths } from "@/config/Paths";
|
||||
@@ -29,6 +30,20 @@ const AdminList: FC = () => {
|
||||
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: "avatarUrl",
|
||||
title: "آواتار",
|
||||
render: (item) =>
|
||||
item.avatarUrl ? (
|
||||
<PresignedImage
|
||||
src={item.avatarUrl}
|
||||
className="w-10 h-10 rounded-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "fullName",
|
||||
title: "نام و نام خانوادگی",
|
||||
|
||||
+52
-14
@@ -1,12 +1,15 @@
|
||||
import { useFormik } from 'formik';
|
||||
import { type FC, useEffect } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import type { CreateAdminType } from './types/Types';
|
||||
import * as Yup from 'yup';
|
||||
import Button from '@/components/Button';
|
||||
import { Edit } from 'iconsax-react';
|
||||
import Input from '@/components/Input';
|
||||
import Select from '@/components/Select';
|
||||
import UploadBox from '@/components/UploadBox';
|
||||
import PresignedImage from '@/components/PresignedImage';
|
||||
import { useGetAdminById, useGetRoles, useUpdateAdmin } from './hooks/useAdminData';
|
||||
import { useSingleUpload } from '@/pages/uploader/hooks/useUploader';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { toast } from 'react-toastify';
|
||||
import { extractErrorMessage } from '@/config/func';
|
||||
@@ -18,6 +21,22 @@ const UpdateAdmin: FC = () => {
|
||||
const { mutate: update, isPending: isUpdating } = useUpdateAdmin();
|
||||
const { data: rolesData } = useGetRoles();
|
||||
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>({
|
||||
initialValues: {
|
||||
@@ -25,6 +44,7 @@ const UpdateAdmin: FC = () => {
|
||||
lastName: '',
|
||||
phone: '',
|
||||
roleId: '',
|
||||
avatarUrl: undefined,
|
||||
notificationPrefrences: ['test']
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
@@ -40,17 +60,17 @@ const UpdateAdmin: FC = () => {
|
||||
roleId: Yup.string().required('نقش اجباری می باشد'),
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
if (!adminId) return;
|
||||
|
||||
update({ adminId: adminId, params: values }, {
|
||||
onSuccess: () => {
|
||||
toast.success('ادمین با موفقیت ویرایش شد');
|
||||
navigate(Paths.admin.list);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error));
|
||||
},
|
||||
});
|
||||
if (file) {
|
||||
upload(file, {
|
||||
onSuccess: (data) => {
|
||||
values.avatarUrl = data?.data?.key;
|
||||
handleSave(values);
|
||||
},
|
||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||
});
|
||||
} else {
|
||||
handleSave(values);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -62,6 +82,7 @@ const UpdateAdmin: FC = () => {
|
||||
lastName: adminData?.data.lastName || '',
|
||||
phone: '0' + adminData?.data.phone || '',
|
||||
roleId: adminData?.data.role?.id || '',
|
||||
avatarUrl: adminData?.data.avatarUrl || undefined,
|
||||
notificationPrefrences: ['test']
|
||||
});
|
||||
}
|
||||
@@ -101,7 +122,7 @@ const UpdateAdmin: FC = () => {
|
||||
<Button
|
||||
className='w-fit px-6'
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isUpdating}
|
||||
isLoading={isUpdating || isUploading}
|
||||
>
|
||||
<div className='flex gap-1.5'>
|
||||
<Edit size={18} color='black' />
|
||||
@@ -111,7 +132,24 @@ const UpdateAdmin: FC = () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
label='نام'
|
||||
placeholder='نام را وارد کنید'
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface AdminItemType extends RowDataType {
|
||||
isSystem: boolean;
|
||||
};
|
||||
phone: string;
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
export type AdminsType = BaseResponse<AdminItemType[]>;
|
||||
@@ -25,6 +26,7 @@ export type CreateAdminType = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
roleId: string;
|
||||
avatarUrl?: string;
|
||||
notificationPrefrences?: string[];
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import Input from '@/components/Input'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import Select from '@/components/Select'
|
||||
import MultiSelect from '@/components/MultiSelect'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import Button from '@/components/Button'
|
||||
import BackButton from '@/components/BackButton'
|
||||
@@ -72,7 +73,7 @@ const getInitialValues = (invoiceItemId: string | null): ConvertToOrderItemType
|
||||
title: '',
|
||||
description: '',
|
||||
attachments: [],
|
||||
designerId: '',
|
||||
designerIds: [],
|
||||
keepRequestChats: true,
|
||||
})
|
||||
|
||||
@@ -85,7 +86,9 @@ const getValidationSchema = (invoiceItemId: string | null) =>
|
||||
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
||||
title: Yup.string().required('عنوان اجباری است.'),
|
||||
description: Yup.string(),
|
||||
designerId: Yup.string().required('این فیلد اجباری است'),
|
||||
designerIds: Yup.array()
|
||||
.of(Yup.string().required())
|
||||
.min(1, 'انتخاب حداقل یک طراح اجباری است.'),
|
||||
})
|
||||
|
||||
const ConvertToOrders: FC = () => {
|
||||
@@ -234,19 +237,23 @@ const ConvertToOrders: FC = () => {
|
||||
)}
|
||||
|
||||
<div className="rowTwoInput">
|
||||
<Select
|
||||
<MultiSelect
|
||||
items={
|
||||
admins?.data?.map((item) => ({
|
||||
label: item.firstName + ' ' + item.lastName,
|
||||
value: item.id,
|
||||
})) || []
|
||||
}
|
||||
label="طراح"
|
||||
placeholder="انتخاب طراح"
|
||||
{...formik.getFieldProps('designerId')}
|
||||
label="طراحان"
|
||||
placeholder="انتخاب طراحان"
|
||||
value={formik.values.designerIds}
|
||||
onChange={(designerIds) =>
|
||||
formik.setFieldValue('designerIds', designerIds)
|
||||
}
|
||||
onBlur={() => formik.setFieldTouched('designerIds', true)}
|
||||
error_text={
|
||||
formik.touched.designerId && formik.errors.designerId
|
||||
? formik.errors.designerId
|
||||
formik.touched.designerIds && formik.errors.designerIds
|
||||
? String(formik.errors.designerIds)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useFormik } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import MultiSelect from '@/components/MultiSelect'
|
||||
import Textarea from '@/components/Textarea'
|
||||
import UploadBox from '@/components/UploadBox'
|
||||
import { useGetUsers } from '../user/hooks/useUserData'
|
||||
@@ -53,7 +54,7 @@ const EditOrder: FC = () => {
|
||||
title: '',
|
||||
attachments: [],
|
||||
description: '',
|
||||
designerId: '',
|
||||
designerIds: [],
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
userId: Yup.string().required('انتخاب کاربر اجباری است.'),
|
||||
@@ -61,7 +62,9 @@ const EditOrder: FC = () => {
|
||||
estimatedDays: Yup.number().required('تخمین روز اجباری است.').min(0, 'باید عدد مثبت باشد'),
|
||||
productId: Yup.string().required('انتخاب محصول اجباری است.'),
|
||||
title: Yup.string().required('عنوان اجباری است.'),
|
||||
designerId: Yup.string().required('انتخاب طراح اجباری است.'),
|
||||
designerIds: Yup.array()
|
||||
.of(Yup.string().required())
|
||||
.min(1, 'انتخاب حداقل یک طراح اجباری است.'),
|
||||
}),
|
||||
enableReinitialize: true,
|
||||
onSubmit: async (values) => {
|
||||
@@ -96,14 +99,18 @@ const EditOrder: FC = () => {
|
||||
if (order) {
|
||||
formik.setValues({
|
||||
userId: order.user?.id ?? '',
|
||||
invoiceItemId: order.invoiceItem ?? '',
|
||||
invoiceItemId: typeof order.invoiceItem === 'object' && order.invoiceItem
|
||||
? order.invoiceItem.id
|
||||
: '',
|
||||
typeId: order.type?.id ?? '',
|
||||
estimatedDays: order.estimatedDays ?? 0,
|
||||
productId: order.product?.id ?? '',
|
||||
title: order.title ?? '',
|
||||
attachments: normalizeAttachments(order.attachments ?? []),
|
||||
description: (order as OrderDetailDataType & { description?: string })?.description ?? '',
|
||||
designerId: order.designer?.id ?? '',
|
||||
description: order?.description ?? '',
|
||||
designerIds: (order.designers ?? [])
|
||||
.map((item) => item.designer?.id)
|
||||
.filter(Boolean),
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -121,12 +128,14 @@ const EditOrder: FC = () => {
|
||||
value: item.id,
|
||||
})) ?? []
|
||||
|
||||
const currentDesigner = order?.designer
|
||||
if (currentDesigner?.id && !items.some((item) => item.value === currentDesigner.id)) {
|
||||
items.unshift({
|
||||
label: `${currentDesigner.firstName ?? ''} ${currentDesigner.lastName ?? ''}`.trim() || currentDesigner.phone,
|
||||
value: currentDesigner.id,
|
||||
})
|
||||
for (const orderDesigner of order?.designers ?? []) {
|
||||
const currentDesigner = orderDesigner.designer
|
||||
if (currentDesigner?.id && !items.some((item) => item.value === currentDesigner.id)) {
|
||||
items.unshift({
|
||||
label: `${currentDesigner.firstName ?? ''} ${currentDesigner.lastName ?? ''}`.trim() || currentDesigner.phone,
|
||||
value: currentDesigner.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
@@ -188,14 +197,16 @@ const EditOrder: FC = () => {
|
||||
</div>
|
||||
|
||||
<div className='rowTwoInput mt-6'>
|
||||
<Select
|
||||
<MultiSelect
|
||||
items={designerSelectItems}
|
||||
label='طراح'
|
||||
placeholder='انتخاب طراح'
|
||||
{...formik.getFieldProps('designerId')}
|
||||
label='طراحان'
|
||||
placeholder='انتخاب طراحان'
|
||||
value={formik.values.designerIds}
|
||||
onChange={(designerIds) => formik.setFieldValue('designerIds', designerIds)}
|
||||
onBlur={() => formik.setFieldTouched('designerIds', true)}
|
||||
error_text={
|
||||
formik.touched.designerId && formik.errors.designerId
|
||||
? formik.errors.designerId
|
||||
formik.touched.designerIds && formik.errors.designerIds
|
||||
? String(formik.errors.designerIds)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useState } from 'react'
|
||||
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 { getFileNameAndExtensionFromUrl } from '@/config/func'
|
||||
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?.product?.title} />
|
||||
</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>
|
||||
|
||||
{order?.user && (
|
||||
@@ -175,10 +184,10 @@ const OrderDetailSidebar: FC<Props> = ({
|
||||
</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">
|
||||
<h3 className="text-sm font-medium text-gray-900">تیم</h3>
|
||||
{order.creator && (
|
||||
{order?.creator && (
|
||||
<PersonCard
|
||||
title="ایجادکننده"
|
||||
name={getCreatorDisplayName(order.creator)}
|
||||
@@ -186,15 +195,16 @@ const OrderDetailSidebar: FC<Props> = ({
|
||||
phone={order.creator.phone}
|
||||
/>
|
||||
)}
|
||||
{order.designer && (
|
||||
{(order?.designers ?? []).map((orderDesigner) => (
|
||||
<PersonCard
|
||||
key={orderDesigner.id}
|
||||
title="طراح / مجری"
|
||||
name={getCreatorDisplayName(order.designer)}
|
||||
name={getCreatorDisplayName(orderDesigner.designer)}
|
||||
subtitle="مسئول طراحی"
|
||||
phone={order.designer.phone}
|
||||
phone={orderDesigner.designer?.phone}
|
||||
icon="designer"
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -204,19 +214,29 @@ const OrderDetailSidebar: FC<Props> = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
{attachments.map((key, idx) => {
|
||||
const { fileName } = getFileNameAndExtensionFromUrl(key)
|
||||
const handleDownload = async () => {
|
||||
const resolvedUrl = await getPresignedUrl(key)
|
||||
window.open(resolvedUrl, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
key={key}
|
||||
type="button"
|
||||
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"
|
||||
className="flex items-center gap-2 rounded-xl border border-gray-100 bg-gray-50/80 px-3 py-2.5"
|
||||
>
|
||||
<Paperclip2 size={18} className="shrink-0" />
|
||||
<span className="truncate">{fileName || `پیوست ${idx + 1}`}</span>
|
||||
</button>
|
||||
<Paperclip2 size={18} className="shrink-0 text-gray-400" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm text-gray-900 text-right">
|
||||
{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>
|
||||
|
||||
@@ -16,14 +16,20 @@ const getCustomerName = (item: OrderListItemType) => {
|
||||
return fullName || user.phone || "—";
|
||||
};
|
||||
|
||||
const getDesignerName = (item: OrderListItemType) => {
|
||||
const designer = item.designer;
|
||||
if (!designer || typeof designer === "string") {
|
||||
return typeof designer === "string" ? designer : "—";
|
||||
}
|
||||
const getDesignerNames = (item: OrderListItemType) => {
|
||||
const designers = item.designers ?? [];
|
||||
if (!designers.length) return "—";
|
||||
|
||||
const name = [designer.firstName, designer.lastName].filter(Boolean).join(" ");
|
||||
return name || designer.phone || "—";
|
||||
const names = designers
|
||||
.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 = {
|
||||
@@ -59,9 +65,9 @@ export const getOrderListColumns = ({
|
||||
render: (item) => <span>{item.product?.title ?? "—"}</span>,
|
||||
},
|
||||
{
|
||||
key: "designer",
|
||||
key: "designers",
|
||||
title: "مجری / طراح",
|
||||
render: (item) => <span>{getDesignerName(item)}</span>,
|
||||
render: (item) => <span>{getDesignerNames(item)}</span>,
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
|
||||
@@ -88,7 +88,7 @@ export interface OrderListItemType extends RowDataType {
|
||||
title: string;
|
||||
type: string;
|
||||
user: UserType;
|
||||
designer: OrderDetailCreatorType | string | null;
|
||||
designers: OrderDesignerType[];
|
||||
creator: string;
|
||||
orderNumber: number;
|
||||
status: string;
|
||||
@@ -132,6 +132,13 @@ export type OrderDetailCreatorType = {
|
||||
phone: string;
|
||||
};
|
||||
|
||||
export type OrderDesignerType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
designer: OrderDetailCreatorType;
|
||||
};
|
||||
|
||||
export type OrderDetailDataType = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -142,11 +149,12 @@ export type OrderDetailDataType = {
|
||||
title: string;
|
||||
type: OrderDetailCategoryType;
|
||||
user: UserType;
|
||||
designer: OrderDetailCreatorType | null;
|
||||
designers: OrderDesignerType[];
|
||||
creator: OrderDetailCreatorType;
|
||||
orderNumber: number;
|
||||
status: string;
|
||||
estimatedDays: number;
|
||||
description?: string | null;
|
||||
attachments: unknown[];
|
||||
};
|
||||
|
||||
@@ -251,7 +259,7 @@ export type UpdateOrderType = {
|
||||
title: string;
|
||||
attachments: string[];
|
||||
description: string;
|
||||
designerId: string;
|
||||
designerIds: string[];
|
||||
};
|
||||
|
||||
export type ConvertToOrderItemType = {
|
||||
@@ -263,7 +271,7 @@ export type ConvertToOrderItemType = {
|
||||
title: string;
|
||||
description?: string;
|
||||
attachments: string[];
|
||||
designerId: string;
|
||||
designerIds: string[];
|
||||
keepRequestChats: boolean;
|
||||
};
|
||||
|
||||
|
||||
+36
-10
@@ -1,12 +1,14 @@
|
||||
import { useFormik } from "formik";
|
||||
import { type FC } from "react";
|
||||
import { useState, type FC } from "react";
|
||||
import * as Yup from "yup";
|
||||
import Button from "@/components/Button";
|
||||
import { TickCircle } from "iconsax-react";
|
||||
import Input from "@/components/Input";
|
||||
import Textarea from "@/components/Textarea";
|
||||
import SwitchComponent from "@/components/Switch";
|
||||
import UploadBox from "@/components/UploadBox";
|
||||
import { useCreateUser } from "./hooks/useUserData";
|
||||
import { useSingleUpload } from "@/pages/uploader/hooks/useUploader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { extractErrorMessage } from "@/config/func";
|
||||
@@ -20,6 +22,7 @@ const initialValues: CreateUserType = {
|
||||
address: "",
|
||||
gender: true,
|
||||
maxCredit: 0,
|
||||
avatarUrl: undefined,
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
@@ -35,18 +38,34 @@ const validationSchema = Yup.object({
|
||||
const CreateUser: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
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>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit: (values) => {
|
||||
createUser(values, {
|
||||
onSuccess: () => {
|
||||
toast.success("مشتری با موفقیت ایجاد شد");
|
||||
navigate(Paths.users.list);
|
||||
},
|
||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||
});
|
||||
if (file) {
|
||||
upload(file, {
|
||||
onSuccess: (data) => {
|
||||
values.avatarUrl = data?.data?.key;
|
||||
handleSave(values);
|
||||
},
|
||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||
});
|
||||
} else {
|
||||
handleSave(values);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,7 +76,7 @@ const CreateUser: FC = () => {
|
||||
<Button
|
||||
className="w-fit px-6"
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isPending}
|
||||
isLoading={isPending || isUploading}
|
||||
>
|
||||
<div className="flex gap-1.5">
|
||||
<TickCircle size={18} color="black" />
|
||||
@@ -67,7 +86,14 @@ const CreateUser: FC = () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
label="موبایل"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, type FC } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useGetUsers } from './hooks/useUserData'
|
||||
import Table from '@/components/Table'
|
||||
import PresignedImage from '@/components/PresignedImage'
|
||||
import moment from 'moment-jalaali'
|
||||
import Button from '@/components/Button'
|
||||
import { AddSquare, Edit } from 'iconsax-react'
|
||||
@@ -27,6 +28,13 @@ const UsersList: FC = () => {
|
||||
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
key: 'avatarUrl',
|
||||
title: 'آواتار',
|
||||
render: (item) => item.avatarUrl
|
||||
? <PresignedImage src={item.avatarUrl} className='w-10 h-10 rounded-full object-cover' alt='' />
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
key: 'fullName',
|
||||
title: 'نام و نام خانوادگی',
|
||||
|
||||
+50
-12
@@ -1,15 +1,18 @@
|
||||
import { useFormik } from "formik";
|
||||
import { type FC, useEffect } from "react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import * as Yup from "yup";
|
||||
import Button from "@/components/Button";
|
||||
import { TickCircle } from "iconsax-react";
|
||||
import Input from "@/components/Input";
|
||||
import Textarea from "@/components/Textarea";
|
||||
import SwitchComponent from "@/components/Switch";
|
||||
import UploadBox from "@/components/UploadBox";
|
||||
import PresignedImage from "@/components/PresignedImage";
|
||||
import {
|
||||
useGetUserById,
|
||||
useUpdateUser,
|
||||
} from "./hooks/useUserData";
|
||||
import { useSingleUpload } from "@/pages/uploader/hooks/useUploader";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
import { extractErrorMessage } from "@/config/func";
|
||||
@@ -23,6 +26,7 @@ const initialValues: CreateUserType = {
|
||||
address: "",
|
||||
gender: true,
|
||||
maxCredit: 0,
|
||||
avatarUrl: undefined,
|
||||
};
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
@@ -42,22 +46,38 @@ const UpdateUser: FC = () => {
|
||||
!!userId
|
||||
);
|
||||
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>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit: (values) => {
|
||||
if (!userId) return;
|
||||
updateUser(
|
||||
{ userId, params: values },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success("مشتری با موفقیت ویرایش شد");
|
||||
navigate(Paths.users.list);
|
||||
if (file) {
|
||||
upload(file, {
|
||||
onSuccess: (data) => {
|
||||
values.avatarUrl = data?.data?.key;
|
||||
handleSave(values);
|
||||
},
|
||||
onError: (error) => toast.error(extractErrorMessage(error)),
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
handleSave(values);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -71,6 +91,7 @@ const UpdateUser: FC = () => {
|
||||
address: u.addresse ?? "",
|
||||
gender: u.gender ?? true,
|
||||
maxCredit: u.maxCredit ?? 0,
|
||||
avatarUrl: u.avatarUrl ?? undefined,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -103,7 +124,7 @@ const UpdateUser: FC = () => {
|
||||
<Button
|
||||
className="w-fit px-6"
|
||||
onClick={() => formik.handleSubmit()}
|
||||
isLoading={isPending}
|
||||
isLoading={isPending || isUploading}
|
||||
>
|
||||
<div className="flex gap-1.5">
|
||||
<TickCircle size={18} color="black" />
|
||||
@@ -113,7 +134,24 @@ const UpdateUser: FC = () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
label="موبایل"
|
||||
placeholder="۰۹۱۲۳۴۵۶۷۸۹"
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface UserType extends RowDataType {
|
||||
addresse: string | null;
|
||||
maxCredit: number;
|
||||
isActive: boolean;
|
||||
avatarUrl: string | null;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
@@ -23,6 +24,7 @@ export interface CreateUserType {
|
||||
address: string;
|
||||
gender: boolean;
|
||||
maxCredit: number;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export type UserDetailResponseType = BaseResponse<UserType>;
|
||||
|
||||
@@ -48,7 +48,7 @@ const SideBar: FC = () => {
|
||||
)}
|
||||
<div
|
||||
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'
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user