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

This commit is contained in:
2026-07-17 18:50:07 +03:30
parent ac205ccb7a
commit d85e0e49ce
+311 -197
View File
@@ -1,13 +1,23 @@
import { useState, type FC } from 'react' import { useState, type FC, type ReactNode } from 'react'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import * as Yup from 'yup' import * as Yup from 'yup'
import moment from 'moment-jalaali' import moment from 'moment-jalaali'
import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import {
Box1,
Calendar,
DocumentText,
Profile2User,
ReceiptText,
TickSquare,
WalletMoney,
} from 'iconsax-react'
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 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 { useMultiUpload } from '@/pages/uploader/hooks/useUploader' import { useMultiUpload } from '@/pages/uploader/hooks/useUploader'
import type { ConvertToOrderItemType } from './types/Types' import type { ConvertToOrderItemType } from './types/Types'
import { useConverToOrder, useGetProducts } from './hooks/useOrderData' import { useConverToOrder, useGetProducts } from './hooks/useOrderData'
@@ -24,6 +34,35 @@ import { Checkbox } from '@/components/ui/checkbox'
const formatAmount = (amount: number) => const formatAmount = (amount: number) =>
Number(amount).toLocaleString('fa-IR') + ' ریال' Number(amount).toLocaleString('fa-IR') + ' ریال'
type InfoFieldProps = {
label: string
value: ReactNode
icon?: ReactNode
}
const InfoField: FC<InfoFieldProps> = ({ label, value, icon }) => (
<div className="rounded-2xl border border-border/70 bg-[#FBFCFF] p-4">
<div className="mb-2 flex items-center gap-2 text-[12px] text-[#7D8597]">
{icon}
<span>{label}</span>
</div>
<div className="text-[15px] font-medium text-[#1F2937]">{value}</div>
</div>
)
type SummaryRowProps = {
label: string
value: string
valueClassName?: string
}
const SummaryRow: FC<SummaryRowProps> = ({ label, value, valueClassName }) => (
<div className="flex items-center justify-between rounded-xl border border-border/70 bg-white px-4 py-3">
<span className="text-[13px] text-[#6B7280]">{label}</span>
<span className={`text-[14px] font-semibold ${valueClassName ?? ''}`}>{value}</span>
</div>
)
const getInitialValues = (invoiceItemId: string | null): ConvertToOrderItemType => ({ const getInitialValues = (invoiceItemId: string | null): ConvertToOrderItemType => ({
invoiceItemId: invoiceItemId ?? '', invoiceItemId: invoiceItemId ?? '',
productId: '', productId: '',
@@ -46,11 +85,10 @@ 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('این فیلد اجباری است') designerId: Yup.string().required('این فیلد اجباری است'),
}) })
const ConvertToOrders: FC = () => { const ConvertToOrders: FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const invoiceItemId = searchParams.get('invoiceItemId') const invoiceItemId = searchParams.get('invoiceItemId')
@@ -99,7 +137,7 @@ const ConvertToOrders: FC = () => {
}, },
onError: (error) => { onError: (error) => {
toast.error(extractErrorMessage(error)) toast.error(extractErrorMessage(error))
} },
}) })
}, },
}) })
@@ -110,186 +148,150 @@ const ConvertToOrders: FC = () => {
: invoiceItem?.invoice?.user?.phone ?? '-' : invoiceItem?.invoice?.user?.phone ?? '-'
const discounts = invoiceItem ? formatItemDiscountDisplay(invoiceItem) : null const discounts = invoiceItem ? formatItemDiscountDisplay(invoiceItem) : null
const isSubmitting = multiUpload.isPending || isPending
const backTo = invoiceItem?.invoice?.id
? Paths.perfomaInvoice.detail + invoiceItem.invoice.id
: Paths.order.list
return ( return (
<div className="mt-5"> <div className="mt-5 space-y-8">
<h1 className="text-lg font-light">تبدیل به سفارش</h1> <div className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-3">
{invoiceItemId && ( <BackButton to={backTo} />
<div className="mt-8 rounded-3xl bg-white p-6"> <div className="flex items-center gap-2 text-[#111827]">
<div className="font-light">اطلاعات آیتم پیش فاکتور</div> <DocumentText size={20} color="#111827" />
<h1 className="text-xl font-medium">تبدیل به سفارش</h1>
{isInvoiceItemLoading ? ( </div>
<div className="mt-6 flex min-h-[120px] items-center justify-center text-[#888888]"> {invoiceItemId && invoiceItem?.product?.title && (
در حال بارگذاری... <p className="text-sm text-[#6B7280]">
</div> ایجاد سفارش از آیتم «{invoiceItem.product.title}»
) : invoiceItem ? ( </p>
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">پیش فاکتور</div>
<Link
to={Paths.perfomaInvoice.detail + invoiceItem.invoice.id}
className="text-[15px] text-primary hover:underline"
>
#{invoiceItem.invoice.invoiceNumber}
</Link>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">مشتری</div>
<div className="text-[15px]">{customerName}</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">محصول</div>
<div className="text-[15px]">{invoiceItem.product?.title ?? '-'}</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">تعداد</div>
<div className="text-[15px]">{invoiceItem.quantity ?? '-'}</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">مبلغ واحد</div>
<div className="text-[15px]">
{invoiceItem.unitPrice != null
? formatAmount(invoiceItem.unitPrice)
: '-'}
</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">تخفیف (مبلغ)</div>
<div className="text-[15px]">{discounts?.value ?? '-'}</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">تخفیف (درصد)</div>
<div className="text-[15px]">{discounts?.percent ?? '-'}</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">مبلغ کل</div>
<div className="text-[15px] font-semibold">
{formatAmount(invoiceItem.total ?? 0)}
</div>
</div>
<div>
<div className="mb-1.5 text-[13px] text-[#888888]">تاریخ تایید</div>
<div className="text-[15px]">
{invoiceItem.confirmedAt
? moment(invoiceItem.confirmedAt).format('jYYYY/jMM/jDD HH:mm')
: '-'}
</div>
</div>
{invoiceItem.description && (
<div className="sm:col-span-2 lg:col-span-3">
<div className="mb-1.5 text-[13px] text-[#888888]">توضیحات</div>
<div className="whitespace-pre-wrap text-[15px]">
{invoiceItem.description}
</div>
</div>
)}
</div>
) : (
<div className="mt-6 text-[15px] text-red-500">
آیتم پیش فاکتور یافت نشد.
</div>
)} )}
</div> </div>
)}
<div className="bg-white rounded-3xl p-6 mt-8"> <Button
<form type="button"
onSubmit={formik.handleSubmit} className="w-fit px-5"
className="space-y-6" onClick={() => formik.handleSubmit()}
isLoading={isSubmitting}
> >
{!invoiceItemId && ( <div className="flex items-center gap-2">
<div className="rowTwoInput"> <TickSquare size={20} color="black" />
<div className="w-full"> <span>ثبت سفارش</span>
<UserSearch </div>
label="کاربر" </Button>
placeholder="جستجوی کاربر (نام، موبایل، ...)" </div>
value={formik.values.userId}
onChange={(id) => { <div
formik.setFieldValue('userId', id) className={
formik.setFieldTouched('userId', true) invoiceItemId
}} ? 'grid grid-cols-1 gap-6 xl:grid-cols-[minmax(0,1fr)_320px]'
: ''
}
>
<div className="rounded-3xl border border-border/70 bg-white p-6 shadow-sm">
<div className="font-medium text-[#111827]">اطلاعات سفارش</div>
<p className="mt-1 text-[13px] text-[#6B7280]">
جزئیات مورد نیاز برای ثبت سفارش را تکمیل کنید.
</p>
<form onSubmit={formik.handleSubmit} className="mt-6 space-y-6">
{!invoiceItemId && (
<div className="rowTwoInput">
<div className="w-full">
<UserSearch
label="کاربر"
placeholder="جستجوی کاربر (نام، موبایل، ...)"
value={formik.values.userId}
onChange={(id) => {
formik.setFieldValue('userId', id)
formik.setFieldTouched('userId', true)
}}
/>
{formik.touched.userId && formik.errors.userId ? (
<div className="mt-2 mr-2 text-right text-xs font-medium text-red-600">
{formik.errors.userId}
</div>
) : null}
</div>
<Select
items={
products?.data?.map((item) => ({
label: item.title,
value: String(item.id),
})) ?? []
}
label="محصول"
placeholder="انتخاب محصول"
{...formik.getFieldProps('productId')}
error_text={
formik.touched.productId && formik.errors.productId
? formik.errors.productId
: ''
}
/> />
{formik.touched.userId && formik.errors.userId ? (
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
{formik.errors.userId}
</div>
) : null}
</div> </div>
)}
<div className="rowTwoInput">
<Select <Select
items={ items={
products?.data?.map((item) => ({ admins?.data?.map((item) => ({
label: item.firstName + ' ' + item.lastName,
value: item.id,
})) || []
}
label="طراح"
placeholder="انتخاب طراح"
{...formik.getFieldProps('designerId')}
error_text={
formik.touched.designerId && formik.errors.designerId
? formik.errors.designerId
: ''
}
/>
<Select
items={
categories?.data?.map((item) => ({
label: item.title, label: item.title,
value: String(item.id), value: String(item.id),
})) ?? [] })) ?? []
} }
label="محصول" label="نوع"
placeholder="انتخاب محصول" placeholder="انتخاب نوع"
{...formik.getFieldProps('productId')} {...formik.getFieldProps('typeId')}
error_text={ error_text={
formik.touched.productId && formik.errors.productId formik.touched.typeId && formik.errors.typeId
? formik.errors.productId ? formik.errors.typeId
: '' : ''
} }
/> />
</div> </div>
)}
<div className="rowTwoInput"> <div className="rowTwoInput">
<Input
<Select label="تخمین روز"
items={admins?.data?.map((item) => { type="number"
return { {...formik.getFieldProps('estimatedDays')}
label: item.firstName + ' ' + item.lastName, error_text={
value: item.id formik.touched.estimatedDays && formik.errors.estimatedDays
? formik.errors.estimatedDays
: ''
} }
}) || []} />
label="طراح"
placeholder="انتخاب طراح"
{...formik.getFieldProps('designerId')}
error_text={
formik.touched.designerId && formik.errors.designerId ? formik.errors.designerId : ''
}
/>
<Select <Input
items={ label="عنوان"
categories?.data?.map((item) => ({ {...formik.getFieldProps('title')}
label: item.title, error_text={
value: String(item.id), formik.touched.title && formik.errors.title
})) ?? [] ? formik.errors.title
} : ''
label="نوع" }
placeholder="انتخاب نوع" />
{...formik.getFieldProps('typeId')} </div>
error_text={
formik.touched.typeId && formik.errors.typeId ? formik.errors.typeId : ''
}
/>
</div>
<div className="rowTwoInput">
<Input
label="تخمین روز"
type="number"
{...formik.getFieldProps('estimatedDays')}
error_text={
formik.touched.estimatedDays && formik.errors.estimatedDays
? formik.errors.estimatedDays
: ''
}
/>
<Input
label="عنوان"
{...formik.getFieldProps('title')}
error_text={
formik.touched.title && formik.errors.title ? formik.errors.title : ''
}
/>
</div>
<div>
<Textarea <Textarea
label="توضیحات" label="توضیحات"
placeholder="توضیحات را وارد کنید..." placeholder="توضیحات را وارد کنید..."
@@ -300,53 +302,165 @@ const ConvertToOrders: FC = () => {
: '' : ''
} }
/> />
</div>
{invoiceItemId && ( {invoiceItemId && (
<div>
<div <div
className={`flex items-center gap-2 ${ className={`rounded-2xl border border-border/70 bg-[#FBFCFF] p-4 ${
!hasLinkedRequest && !isInvoiceItemLoading !hasLinkedRequest && !isInvoiceItemLoading
? 'pointer-events-none opacity-50' ? 'opacity-60'
: '' : ''
}`} }`}
> >
<Checkbox <div
id="keepRequestChats" className={`flex items-center gap-2 ${
checked={formik.values.keepRequestChats} !hasLinkedRequest && !isInvoiceItemLoading
onCheckedChange={(checked) => ? 'pointer-events-none'
formik.setFieldValue('keepRequestChats', !!checked) : ''
} }`}
/>
<label
htmlFor="keepRequestChats"
className="cursor-pointer select-none text-[13px]"
> >
انتقال چتهای درخواست به سفارش <Checkbox
</label> id="keepRequestChats"
checked={formik.values.keepRequestChats}
onCheckedChange={(checked) =>
formik.setFieldValue('keepRequestChats', !!checked)
}
/>
<label
htmlFor="keepRequestChats"
className="cursor-pointer select-none text-[13px] text-[#1F2937]"
>
انتقال چتهای درخواست به سفارش
</label>
</div>
{!hasLinkedRequest && !isInvoiceItemLoading && (
<p className="mt-2 text-xs text-[#888888]">
این پیشفاکتور به درخواستی متصل نیست.
</p>
)}
</div> </div>
{!hasLinkedRequest && !isInvoiceItemLoading && ( )}
<p className="mt-2 text-xs text-[#888888]">
این پیشفاکتور به درخواستی متصل نیست. <div className="rounded-2xl border border-dashed border-border/70 bg-[#FBFCFF] p-4">
</p> <UploadBox
label="پیوست‌ها"
isMultiple
onChange={setAttachmentFiles}
/>
</div>
<div className="flex justify-end xl:hidden">
<Button type="submit" className="w-fit px-5" isLoading={isSubmitting}>
<div className="flex items-center gap-2">
<TickSquare size={20} color="black" />
<span>ثبت سفارش</span>
</div>
</Button>
</div>
</form>
</div>
{invoiceItemId && (
<aside className="h-fit space-y-4">
<div className="rounded-3xl border border-border/70 bg-[#F7F9FF] p-5 shadow-sm">
<div className="mb-4 flex items-center gap-2 text-[#111827]">
<ReceiptText size={18} color="#111827" />
<span className="font-medium">آیتم پیش فاکتور</span>
</div>
{isInvoiceItemLoading ? (
<div className="flex min-h-[140px] items-center justify-center text-[#888888]">
در حال بارگذاری...
</div>
) : invoiceItem ? (
<div className="space-y-3">
<InfoField
label="پیش فاکتور"
icon={<ReceiptText size={14} color="#7D8597" />}
value={
<Link
to={Paths.perfomaInvoice.detail + invoiceItem.invoice.id}
className="text-primary hover:underline"
>
#{invoiceItem.invoice.invoiceNumber}
</Link>
}
/>
<InfoField
label="مشتری"
value={customerName}
icon={<Profile2User size={14} color="#7D8597" />}
/>
<InfoField
label="محصول"
value={invoiceItem.product?.title ?? '-'}
icon={<Box1 size={14} color="#7D8597" />}
/>
<InfoField
label="تاریخ تایید"
icon={<Calendar size={14} color="#7D8597" />}
value={
invoiceItem.confirmedAt
? moment(invoiceItem.confirmedAt).format(
'jYYYY/jMM/jDD HH:mm',
)
: '-'
}
/>
{invoiceItem.description && (
<div className="rounded-2xl border border-border/70 bg-white p-4">
<div className="mb-2 text-[12px] text-[#7D8597]">
توضیحات
</div>
<div className="whitespace-pre-wrap text-[14px] text-[#1F2937]">
{invoiceItem.description}
</div>
</div>
)}
</div>
) : (
<div className="rounded-xl border border-red-100 bg-red-50 px-3 py-3 text-[13px] text-red-700">
آیتم پیش فاکتور یافت نشد.
</div>
)} )}
</div> </div>
)}
<div> {invoiceItem && !isInvoiceItemLoading && (
<UploadBox <div className="rounded-3xl border border-border/70 bg-[#F7F9FF] p-5 shadow-sm">
label="پیوست‌ها" <div className="mb-4 flex items-center gap-2 text-[#111827]">
isMultiple <WalletMoney size={18} color="#111827" />
onChange={setAttachmentFiles} <span className="font-medium">خلاصه مالی</span>
/> </div>
</div> <div className="space-y-2">
<SummaryRow
<div className="flex justify-end"> label="تعداد"
<Button type="submit" isLoading={multiUpload.isPending || isPending}> value={String(invoiceItem.quantity ?? '-')}
ثبت />
</Button> <SummaryRow
</div> label="مبلغ واحد"
</form> value={
invoiceItem.unitPrice != null
? formatAmount(invoiceItem.unitPrice)
: '-'
}
/>
<SummaryRow
label="تخفیف (مبلغ)"
value={discounts?.value ?? '-'}
/>
<SummaryRow
label="تخفیف (درصد)"
value={discounts?.percent ?? '-'}
/>
<SummaryRow
label="مبلغ کل"
value={formatAmount(invoiceItem.total ?? 0)}
valueClassName="text-[#111827]"
/>
</div>
</div>
)}
</aside>
)}
</div> </div>
</div> </div>
) )