order detail
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-17 20:18:59 +03:30
parent dcb2c1326c
commit a5724369da
16 changed files with 481 additions and 105 deletions
+169
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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='نام را وارد کنید'
+15
View File
@@ -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
View File
@@ -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='نام را وارد کنید'
+2
View File
@@ -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 -8
View File
@@ -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)
: ''
}
/>
+28 -17
View File
@@ -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",
+12 -4
View File
@@ -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
View File
@@ -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="۰۹۱۲۳۴۵۶۷۸۹"
+8
View File
@@ -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
View File
@@ -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="۰۹۱۲۳۴۵۶۷۸۹"
+2
View File
@@ -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>;
+1 -1
View File
@@ -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'
)}
>