Files
dzone-admin/src/pages/bill/Create.tsx
T
2026-02-17 13:01:35 +03:30

268 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useGetAllCompanies } from '../company/hooks/useCompanyData'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { useDropzone } from 'react-dropzone'
import { CreateBillType } from './types/Types'
import { useCreateBill, useCreateBillWithExcel } from './hooks/useBillData'
import Button from '../../components/Button'
import Input from '../../components/Input'
import { DocumentUpload, TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import { ErrorType } from '../../helpers/types'
import { Pages } from '../../config/Pages'
import { useNavigate } from 'react-router-dom'
import { CompanyItemType } from '../company/types/CompanyTypes'
import PageLoading from '../../components/PageLoading'
const CreateBill: FC = () => {
const navigate = useNavigate()
const getCompanies = useGetAllCompanies()
const createBill = useCreateBill()
const createBillWithExcel = useCreateBillWithExcel()
const [excelFile, setExcelFile] = useState<File | null>(null)
const onDropExcel = useCallback((acceptedFiles: File[]) => {
setExcelFile(acceptedFiles[0] ?? null)
}, [])
const { getRootProps, getInputProps } = useDropzone({
onDrop: onDropExcel,
accept: {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
'application/vnd.ms-excel': ['.xls'],
},
maxFiles: 1,
multiple: false,
})
const companies: CompanyItemType[] = getCompanies.data?.data?.companies ?? []
const formik = useFormik<CreateBillType>({
initialValues: {
waterRate: 0,
dueDays: 0,
values: [],
},
enableReinitialize: true,
validationSchema: Yup.object({
waterRate: Yup.number().min(0, 'نرخ آب باید عدد مثبت باشد').required('نرخ آب الزامی است'),
dueDays: Yup.number().min(0, 'تعداد روز مهلت باید عدد مثبت باشد').required('تعداد روز مهلت الزامی است'),
values: Yup.array()
.of(
Yup.object({
companyId: Yup.string().required(),
waterUsage: Yup.number().min(0, 'مصرف آب باید عدد مثبت باشد').required('مصرف آب الزامی است'),
})
)
.min(1, 'حداقل یک شرکت باید مصرف داشته باشد')
.required('اطلاعات مصرف شرکت‌ها الزامی است'),
}),
onSubmit: async (values) => {
const filteredValues = values.values.filter((v) => v.companyId && (v.waterUsage ?? 0) >= 0)
if (filteredValues.length === 0) {
toast.error('حداقل یک شرکت با مصرف معتبر وارد کنید')
return
}
values.dueDays = Number(values.dueDays)
values.waterRate = Number(values.waterRate)
await createBill.mutateAsync(
{ ...values, values: filteredValues },
{
onSuccess: () => {
toast.success('قبض آب با موفقیت ثبت شد')
navigate(Pages.bill.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message?.[0] ?? 'خطا در ثبت قبض')
},
}
)
},
})
const initializedRef = useRef(false)
useEffect(() => {
if (companies.length > 0 && !initializedRef.current) {
initializedRef.current = true
formik.setFieldValue(
'values',
companies.map((c) => ({ companyId: c.id, waterUsage: 0 }))
)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [companies])
if (getCompanies.isLoading) {
return <PageLoading />
}
if (companies.length === 0 && !getCompanies.isLoading) {
return (
<div className="mt-4 p-6 bg-white rounded-3xl text-center text-gray-600">
شرکتی ثبت نشده است. ابتدا شرکت اضافه کنید.
</div>
)
}
const setWaterUsage = (companyId: string, waterUsage: number) => {
const next = formik.values.values.map((v) =>
v.companyId === companyId ? { ...v, waterUsage } : v
)
formik.setFieldValue('values', next)
}
const handleSubmitExcel = () => {
const waterRate = Number(formik.values.waterRate)
const dueDays = Number(formik.values.dueDays)
if (waterRate < 0 || dueDays < 0) {
toast.error('نرخ آب و تعداد روز مهلت را وارد کنید')
return
}
if (!excelFile) {
toast.error('فایل اکسل را انتخاب کنید')
return
}
createBillWithExcel.mutate(
{ waterRate, dueDays, file: excelFile },
{
onSuccess: () => {
toast.success('قبض آب با موفقیت از طریق اکسل ثبت شد')
navigate(Pages.bill.list)
},
onError: (error: ErrorType) => {
toast.error(error.response?.data?.error?.message?.[0] ?? 'خطا در ثبت قبض با اکسل')
},
}
)
}
return (
<div className="mt-4">
<div className="flex justify-between items-center flex-wrap gap-3">
<div className="text-lg font-medium">افزودن قبض آب</div>
<div className="flex items-center gap-2 flex-wrap">
<Button
className="w-fit px-4"
onClick={() => formik.handleSubmit()}
isLoading={createBill.isPending}
>
<div className="flex items-center gap-2">
<TickCircle size={20} color="white" />
<span>ثبت قبض</span>
</div>
</Button>
<div
{...getRootProps()}
className="flex items-center gap-2 border border-border border-dashed rounded-xl pl-2 pr-1 py-1 bg-gray-50 min-h-10 cursor-pointer hover:bg-gray-100 transition-colors"
>
<input {...getInputProps()} />
<div className="flex items-center gap-2 text-sm text-gray-600 whitespace-nowrap pointer-events-none">
<DocumentUpload size={18} className="shrink-0" />
<span>فایل اکسل</span>
</div>
{excelFile ? (
<span className="text-xs text-gray-700 max-w-[140px] truncate pointer-events-none" title={excelFile.name}>
{excelFile.name}
</span>
) : (
<span className="text-xs text-gray-400 pointer-events-none">کلیک یا فایل را اینجا رها کنید</span>
)}
<Button
type="button"
className="w-fit px-4 bg-secondary text-black hover:bg-secondary/90 shrink-0"
onClick={(e) => {
e.stopPropagation()
handleSubmitExcel()
}}
isLoading={createBillWithExcel.isPending}
>
<span>ثبت با اکسل</span>
</Button>
</div>
</div>
</div>
<div className="mt-10 bg-white py-8 xl:px-10 px-4 rounded-3xl">
<div className="text-base font-medium mb-6">اطلاعات قبض</div>
<div className="rowTwoInput mt-6">
<Input
label="نرخ آب (تومان)"
name="waterRate"
type="number"
min={0}
value={formik.values.waterRate}
onChange={formik.handleChange}
error_text={formik.touched.waterRate && formik.errors.waterRate ? String(formik.errors.waterRate) : ''}
/>
<Input
label="تعداد روز مهلت پرداخت"
name="dueDays"
type="number"
min={0}
value={formik.values.dueDays}
onChange={formik.handleChange}
error_text={formik.touched.dueDays && formik.errors.dueDays ? String(formik.errors.dueDays) : ''}
/>
</div>
<div className="mt-8">
<div className="mb-4">
<span className="text-sm font-medium">مصرف آب به تفکیک شرکت</span>
<p className="text-xs text-gray-500 mt-1">مصرف آب هر شرکت را در ردیف مربوطه وارد کنید.</p>
</div>
<div className="border border-border rounded-2xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="text-right py-3 px-4 font-medium border-b border-border">ردیف</th>
<th className="text-right py-3 px-4 font-medium border-b border-border">نام شرکت</th>
<th className="text-right py-3 px-4 font-medium border-b border-border w-48">مصرف آب</th>
</tr>
</thead>
<tbody>
{companies.map((company, index) => {
const value = formik.values.values.find((v) => v.companyId === company.id)
const usage = value?.waterUsage ?? 0
return (
<tr key={company.id} className="border-b border-border last:border-0 hover:bg-gray-50/50">
<td className="py-3 px-4 text-gray-600">{index + 1}</td>
<td className="py-3 px-4 font-medium">{company.name}</td>
<td className="py-2 px-4">
<Input
name={`usage-${company.id}`}
type="number"
min={0}
value={usage}
onChange={(e) => setWaterUsage(company.id, Number(e.target.value) || 0)}
error_text={
Array.isArray(formik.errors.values) &&
formik.errors.values[index] &&
(formik.errors.values[index] as { waterUsage?: string })?.waterUsage
? (formik.errors.values[index] as { waterUsage: string }).waterUsage
: ''
}
className="!mb-0"
/>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{formik.touched.values && typeof formik.errors.values === 'string' && (
<div className="text-xs text-red-600 mt-2 font-medium">{formik.errors.values}</div>
)}
</div>
</div>
</div>
)
}
export default CreateBill