This commit is contained in:
@@ -12,7 +12,7 @@ import { extractErrorMessage } from '@/config/func'
|
|||||||
|
|
||||||
const LoginStep1: FC = () => {
|
const LoginStep1: FC = () => {
|
||||||
|
|
||||||
const { phone, setPhone, setStepLogin } = useAuthStore()
|
const { phone, setPhone, setStepLogin, setDevOtpCode } = useAuthStore()
|
||||||
const loginWithOtp = useLoginWithOtp()
|
const loginWithOtp = useLoginWithOtp()
|
||||||
const checkHasAccount = useCheckHasAccount()
|
const checkHasAccount = useCheckHasAccount()
|
||||||
|
|
||||||
@@ -27,7 +27,10 @@ const LoginStep1: FC = () => {
|
|||||||
onSubmit(values) {
|
onSubmit(values) {
|
||||||
setPhone(values.phone_email)
|
setPhone(values.phone_email)
|
||||||
loginWithOtp.mutate({ phone: values.phone_email }, {
|
loginWithOtp.mutate({ phone: values.phone_email }, {
|
||||||
onSuccess() {
|
onSuccess(data) {
|
||||||
|
// TODO: remove before production — dev-only OTP autofill
|
||||||
|
const code = data?.data?.code
|
||||||
|
if (code) setDevOtpCode(String(code))
|
||||||
setStepLogin(2)
|
setStepLogin(2)
|
||||||
toast('کد تایید ارسال شد', 'success')
|
toast('کد تایید ارسال شد', 'success')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Paths } from '@/config/Paths'
|
|||||||
|
|
||||||
const LoginStep2: FC = () => {
|
const LoginStep2: FC = () => {
|
||||||
|
|
||||||
const { phone, setStepLogin } = useAuthStore()
|
const { phone, setStepLogin, devOtpCode, setDevOtpCode } = useAuthStore()
|
||||||
const { value, reset } = useCountDown(2, true)
|
const { value, reset } = useCountDown(2, true)
|
||||||
const otpVerify = useOtpVerify()
|
const otpVerify = useOtpVerify()
|
||||||
const loginWithOtp = useLoginWithOtp()
|
const loginWithOtp = useLoginWithOtp()
|
||||||
@@ -24,7 +24,8 @@ const LoginStep2: FC = () => {
|
|||||||
const formik = useFormik<OtpVerifyType>({
|
const formik = useFormik<OtpVerifyType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
phone: phone,
|
phone: phone,
|
||||||
otp: ''
|
// TODO: remove before production — dev-only OTP autofill
|
||||||
|
otp: devOtpCode,
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
otp: Yup.string()
|
otp: Yup.string()
|
||||||
@@ -81,7 +82,14 @@ const LoginStep2: FC = () => {
|
|||||||
|
|
||||||
const reSend = () => {
|
const reSend = () => {
|
||||||
loginWithOtp.mutate({ phone: phone }, {
|
loginWithOtp.mutate({ phone: phone }, {
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
|
// TODO: remove before production — dev-only OTP autofill
|
||||||
|
const code = data?.data?.code
|
||||||
|
if (code) {
|
||||||
|
const otp = String(code)
|
||||||
|
setDevOtpCode(otp)
|
||||||
|
formik.setFieldValue('otp', otp)
|
||||||
|
}
|
||||||
reset()
|
reset()
|
||||||
toast('کد مجدد ارسال شد', 'success')
|
toast('کد مجدد ارسال شد', 'success')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,4 +14,8 @@ export const useAuthStore = create<AuthStoreType>((set) => ({
|
|||||||
setStepLogin(value) {
|
setStepLogin(value) {
|
||||||
set({ stepLogin: value });
|
set({ stepLogin: value });
|
||||||
},
|
},
|
||||||
|
devOtpCode: "",
|
||||||
|
setDevOtpCode(value) {
|
||||||
|
set({ devOtpCode: value });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type AuthStoreType = {
|
|||||||
setEmail: (value: string) => void;
|
setEmail: (value: string) => void;
|
||||||
stepLogin: number;
|
stepLogin: number;
|
||||||
setStepLogin: (value: number) => void;
|
setStepLogin: (value: number) => void;
|
||||||
|
/** TODO: remove before production — dev-only OTP from API response */
|
||||||
|
devOtpCode: string;
|
||||||
|
setDevOtpCode: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginWithPasswordType = {
|
export type LoginWithPasswordType = {
|
||||||
|
|||||||
+103
-26
@@ -1,21 +1,29 @@
|
|||||||
import { type FC } from 'react'
|
import { useState, type FC } from 'react'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import { useGetSections } from '../print/hooks/usePrintData'
|
import { useGetSections } from '../print/hooks/usePrintData'
|
||||||
import { useGetOrderPrint, useSubmitOrderPrint } from './hooks/useOrderData'
|
import { useGetOrderPrint, useSubmitOrderPrint } from './hooks/useOrderData'
|
||||||
import type { OrderPrintFormType } from './types/Types'
|
import type { OrderPrintFormType } from './types/Types'
|
||||||
import ManageForms from '../print/components/ManageForms'
|
import PrintSectionCard from './components/PrintSectionCard'
|
||||||
|
import PrintSectionNav from './components/PrintSectionNav'
|
||||||
|
import { ORDER_PRINT_STORAGE_KEY } from './PrintPreview'
|
||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
|
import BackButton from '@/components/BackButton'
|
||||||
|
import RefreshButton from '@/components/RefreshButton'
|
||||||
import { TickSquare } from 'iconsax-react'
|
import { TickSquare } from 'iconsax-react'
|
||||||
import { toast } from '@/shared/toast'
|
import { toast } from '@/shared/toast'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { Paths } from '@/config/Paths'
|
||||||
|
|
||||||
const OrderPrint: FC = () => {
|
const OrderPrint: FC = () => {
|
||||||
const { id: orderId } = useParams()
|
const { id: orderId } = useParams()
|
||||||
const navigate = useNavigate()
|
const { data, refetch: refetchSections, isFetching: isSectionsFetching } = useGetSections()
|
||||||
const { data } = useGetSections()
|
|
||||||
const { isPending, mutate } = useSubmitOrderPrint()
|
const { isPending, mutate } = useSubmitOrderPrint()
|
||||||
const { data: print } = useGetOrderPrint(orderId!)
|
const { data: print, refetch: refetchPrint, isFetching: isPrintFetching } = useGetOrderPrint(orderId!)
|
||||||
|
|
||||||
|
const sections = data?.data ?? []
|
||||||
|
|
||||||
|
const [selectedSections, setSelectedSections] = useState<Set<string>>(() => new Set())
|
||||||
|
|
||||||
const formik = useFormik<OrderPrintFormType>({
|
const formik = useFormik<OrderPrintFormType>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@@ -29,7 +37,6 @@ const OrderPrint: FC = () => {
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast('با موفقیت ذخیره شد', 'success')
|
toast('با موفقیت ذخیره شد', 'success')
|
||||||
navigate(-1)
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(extractErrorMessage(error), 'error')
|
toast(extractErrorMessage(error), 'error')
|
||||||
@@ -39,36 +46,106 @@ const OrderPrint: FC = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const toggleSection = (id: string) => {
|
||||||
|
setSelectedSections((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(id)) next.delete(id)
|
||||||
|
else next.add(id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAll = (checked: boolean) => {
|
||||||
|
setSelectedSections(checked ? new Set(sections.map((s) => s.id)) : new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectFilledSections = () => {
|
||||||
|
const filledFieldIds = new Set(
|
||||||
|
formik.values.fields
|
||||||
|
.filter((f) => f.value?.trim())
|
||||||
|
.map((f) => f.fieldId)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filledSectionIds = sections
|
||||||
|
.filter((section) => section.fields.some((field) => filledFieldIds.has(field.id)))
|
||||||
|
.map((s) => s.id)
|
||||||
|
|
||||||
|
if (filledSectionIds.length === 0) {
|
||||||
|
toast('هیچ بخشی با مقدار پر شده یافت نشد', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedSections(new Set(filledSectionIds))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrint = () => {
|
||||||
|
if (selectedSections.size === 0) {
|
||||||
|
toast('حداقل یک بخش را انتخاب کنید', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!orderId) return
|
||||||
|
|
||||||
|
sessionStorage.setItem(
|
||||||
|
ORDER_PRINT_STORAGE_KEY(orderId),
|
||||||
|
JSON.stringify({ fields: formik.values.fields })
|
||||||
|
)
|
||||||
|
|
||||||
|
const sectionIds = Array.from(selectedSections).join(',')
|
||||||
|
window.open(`${Paths.orderPrint}${orderId}/preview?sections=${sectionIds}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
refetchSections()
|
||||||
|
refetchPrint()
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRefreshing = isSectionsFetching || isPrintFetching
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mt-5'>
|
<div className="mt-5">
|
||||||
<div className='flex justify-between items-center'>
|
<div className="flex justify-between items-center">
|
||||||
<h1 className='text-lg font-light'>فرم چاپ سفارش</h1>
|
<div className="flex items-center gap-4">
|
||||||
|
<BackButton to={Paths.order.list} />
|
||||||
|
<h1 className="text-lg font-light">فرم چاپ سفارش</h1>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<RefreshButton onClick={handleRefresh} isLoading={isRefreshing} />
|
||||||
<Button
|
<Button
|
||||||
className='w-fit px-6'
|
className="w-fit px-6"
|
||||||
isLoading={isPending}
|
isLoading={isPending}
|
||||||
onClick={() => formik.handleSubmit()}
|
onClick={() => formik.handleSubmit()}
|
||||||
>
|
>
|
||||||
<div className='flex gap-1.5'>
|
<div className="flex gap-1.5">
|
||||||
<TickSquare size={18} color='black' />
|
<TickSquare size={18} color="black" />
|
||||||
<div className='text-[13px] font-light'>ذخیره</div>
|
<div className="text-[13px] font-light">ذخیره</div>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-5 mt-6'>
|
|
||||||
{data?.data?.map((section) => (
|
|
||||||
<div
|
|
||||||
key={section.id}
|
|
||||||
className='bg-white p-6 rounded-3xl'
|
|
||||||
>
|
|
||||||
<div className='font-bold'>{section.title}</div>
|
|
||||||
<ManageForms
|
|
||||||
formik={formik}
|
|
||||||
attributes={section.fields}
|
|
||||||
formFieldKey='fields'
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-6 mt-6 items-start">
|
||||||
|
<div className="flex-1 flex flex-col gap-5 min-w-0">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<PrintSectionCard
|
||||||
|
key={section.id}
|
||||||
|
title={section.title}
|
||||||
|
fields={section.fields}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sections.length > 0 && (
|
||||||
|
<PrintSectionNav
|
||||||
|
sections={sections}
|
||||||
|
selectedIds={selectedSections}
|
||||||
|
onToggle={toggleSection}
|
||||||
|
onToggleAll={toggleAll}
|
||||||
|
onSelectFilled={selectFilledSections}
|
||||||
|
onPrint={handlePrint}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { useEffect, useMemo, type FC } from 'react'
|
||||||
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
|
import { useGetSections } from '@/pages/print/hooks/usePrintData'
|
||||||
|
import { useGetOrderPrint } from './hooks/useOrderData'
|
||||||
|
import PrintFieldDisplay from './components/PrintFieldDisplay'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Printer } from 'iconsax-react'
|
||||||
|
|
||||||
|
const ORDER_PRINT_STORAGE_KEY = (orderId: string) => `order-print-draft-${orderId}`
|
||||||
|
|
||||||
|
const OrderPrintPreview: FC = () => {
|
||||||
|
const { id: orderId } = useParams()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const { data: sectionsData } = useGetSections()
|
||||||
|
const { data: printData } = useGetOrderPrint(orderId!)
|
||||||
|
|
||||||
|
const selectedSectionIds = useMemo(() => {
|
||||||
|
const param = searchParams.get('sections')
|
||||||
|
return param ? param.split(',').filter(Boolean) : []
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const fieldValues = useMemo(() => {
|
||||||
|
const stored = sessionStorage.getItem(ORDER_PRINT_STORAGE_KEY(orderId!))
|
||||||
|
if (stored) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(stored) as { fields: { fieldId: string; value: string }[] }
|
||||||
|
return parsed.fields ?? []
|
||||||
|
} catch {
|
||||||
|
// fall through to API data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return printData?.data?.fileds ?? []
|
||||||
|
}, [orderId, printData?.data?.fileds])
|
||||||
|
|
||||||
|
const sections = useMemo(
|
||||||
|
() =>
|
||||||
|
(sectionsData?.data ?? []).filter((s) => selectedSectionIds.includes(s.id)),
|
||||||
|
[sectionsData?.data, selectedSectionIds]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePrint = () => window.print()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = 'چاپ فرم سفارش'
|
||||||
|
|
||||||
|
const html = document.documentElement
|
||||||
|
const root = document.getElementById('root')
|
||||||
|
|
||||||
|
const enableScroll = (el: HTMLElement) => {
|
||||||
|
el.style.setProperty('overflow', 'auto', 'important')
|
||||||
|
el.style.setProperty('height', 'auto', 'important')
|
||||||
|
el.style.setProperty('min-height', '100%', 'important')
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetScroll = (el: HTMLElement) => {
|
||||||
|
el.style.removeProperty('overflow')
|
||||||
|
el.style.removeProperty('height')
|
||||||
|
el.style.removeProperty('min-height')
|
||||||
|
}
|
||||||
|
|
||||||
|
enableScroll(html)
|
||||||
|
enableScroll(document.body)
|
||||||
|
if (root) enableScroll(root)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
resetScroll(html)
|
||||||
|
resetScroll(document.body)
|
||||||
|
if (root) resetScroll(root)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!sections.length) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-white p-8">
|
||||||
|
<p className="text-gray-500">بخشی برای چاپ انتخاب نشده است.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="print-preview-page min-h-screen bg-white overflow-y-auto pb-16">
|
||||||
|
<div className="no-print fixed top-4 left-4 z-50 flex gap-3">
|
||||||
|
<Button className="w-auto px-6 shadow-lg" onClick={handlePrint}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Printer size={18} color="black" />
|
||||||
|
<span className="text-[13px] font-medium">چاپ</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-auto px-6 shadow-lg bg-gray-200 !text-gray-700"
|
||||||
|
onClick={() => window.close()}
|
||||||
|
>
|
||||||
|
<span className="text-[13px] font-medium">بستن</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-[210mm] mx-auto p-8 print:p-0">
|
||||||
|
<header className="text-center mb-8 pb-4 border-b-2 border-gray-800">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">فرم چاپ سفارش</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
{new Date().toLocaleDateString('fa-IR')}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<section
|
||||||
|
key={section.id}
|
||||||
|
className="flex border border-gray-300 rounded-lg overflow-hidden break-inside-avoid"
|
||||||
|
>
|
||||||
|
<div className="bg-gray-100 flex items-center justify-center px-3 py-5 shrink-0 w-[64px] border-l border-gray-200">
|
||||||
|
<h2 className="text-lg font-bold text-gray-700 [writing-mode:vertical-rl] rotate-180 whitespace-nowrap">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-5">
|
||||||
|
<PrintFieldDisplay
|
||||||
|
fields={section.fields}
|
||||||
|
values={fieldValues}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="mt-8 pt-4 border-t border-gray-200 text-center text-xs text-gray-400 print:mt-6">
|
||||||
|
نگاره — فرم چاپ سفارش
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@media screen {
|
||||||
|
html, body, #root {
|
||||||
|
overflow: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
.print-preview-page { background: white !important; overflow: visible !important; }
|
||||||
|
html, body, #root { overflow: visible !important; height: auto !important; background: white !important; }
|
||||||
|
@page { margin: 15mm; size: A4; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ORDER_PRINT_STORAGE_KEY }
|
||||||
|
export default OrderPrintPreview
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import type { EntityType } from '@/pages/formBuilder/types/Types'
|
||||||
|
import type { OrderPrintFieldType } from '../types/Types'
|
||||||
|
import { FieldTypeEnum } from '@/pages/formBuilder/enum/Enum'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
fields: EntityType[]
|
||||||
|
values: OrderPrintFieldType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrintFieldDisplay: FC<Props> = ({ fields, values }) => {
|
||||||
|
const getValue = (fieldId: string) =>
|
||||||
|
values.find((v) => v.fieldId === fieldId)?.value ?? ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
|
||||||
|
{fields.map((field) => {
|
||||||
|
const value = getValue(field.id)
|
||||||
|
const displayValue =
|
||||||
|
field.type === FieldTypeEnum.checkbox && value
|
||||||
|
? value.split(',').map((v) => v.trim()).filter(Boolean).join('، ')
|
||||||
|
: value || '—'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={field.id}
|
||||||
|
className={
|
||||||
|
field.type === FieldTypeEnum.textarea
|
||||||
|
? 'sm:col-span-2'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="text-xs text-gray-500 mb-1">{field.name}</div>
|
||||||
|
<div className="text-sm text-gray-900 font-medium border-b border-gray-200 pb-1.5 min-h-[28px]">
|
||||||
|
{displayValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrintFieldDisplay
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import type { FormikProps } from 'formik'
|
||||||
|
import type { EntityType } from '@/pages/formBuilder/types/Types'
|
||||||
|
import type { OrderPrintFormType } from '../types/Types'
|
||||||
|
import ManageForms from '@/pages/print/components/ManageForms'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
fields: EntityType[]
|
||||||
|
formik: FormikProps<OrderPrintFormType>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrintSectionCard: FC<Props> = ({ title, fields, formik }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex bg-white rounded-2xl overflow-hidden border border-gray-200 shadow-sm min-h-[100px]">
|
||||||
|
<div className="bg-gray-100 flex items-center justify-center px-3 py-6 shrink-0 w-[72px]">
|
||||||
|
<h2 className="text-xl font-bold text-gray-700 [writing-mode:vertical-rl] rotate-180 whitespace-nowrap tracking-wide">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
<ManageForms
|
||||||
|
formik={formik}
|
||||||
|
attributes={fields}
|
||||||
|
formFieldKey="fields"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrintSectionCard
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { type FC } from 'react'
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { Printer } from 'iconsax-react'
|
||||||
|
import type { SectionItemType } from '@/pages/print/types/Types'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
sections: SectionItemType[]
|
||||||
|
selectedIds: Set<string>
|
||||||
|
onToggle: (id: string) => void
|
||||||
|
onToggleAll: (checked: boolean) => void
|
||||||
|
onSelectFilled: () => void
|
||||||
|
onPrint: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PrintSectionNav: FC<Props> = ({
|
||||||
|
sections,
|
||||||
|
selectedIds,
|
||||||
|
onToggle,
|
||||||
|
onToggleAll,
|
||||||
|
onSelectFilled,
|
||||||
|
onPrint,
|
||||||
|
}) => {
|
||||||
|
const allSelected = sections.length > 0 && selectedIds.size === sections.length
|
||||||
|
const someSelected = selectedIds.size > 0 && selectedIds.size < sections.length
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="w-80 shrink-0 sticky top-4 self-start">
|
||||||
|
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm p-5 flex flex-col gap-4">
|
||||||
|
<h3 className="text-sm font-bold text-gray-800 border-b border-gray-100 pb-3">
|
||||||
|
بخشهای چاپ
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2 pb-2 border-b border-gray-100">
|
||||||
|
<label className="flex items-center gap-2.5 cursor-pointer">
|
||||||
|
<Checkbox
|
||||||
|
checked={allSelected ? true : someSelected ? 'indeterminate' : false}
|
||||||
|
onCheckedChange={(checked) => onToggleAll(checked === true)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-600">انتخاب همه</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onSelectFilled}
|
||||||
|
className="text-xs text-[#8C90A3] hover:text-black border border-border rounded-lg px-2.5 py-1.5 transition-colors shrink-0 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
دارای مقدار
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{sections.map((section) => (
|
||||||
|
<label
|
||||||
|
key={section.id}
|
||||||
|
className="flex items-center gap-2.5 cursor-pointer hover:bg-gray-50 rounded-lg p-1.5 -mx-1.5 transition-colors"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedIds.has(section.id)}
|
||||||
|
onCheckedChange={() => onToggle(section.id)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700 leading-snug">{section.title}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-2 w-full"
|
||||||
|
onClick={onPrint}
|
||||||
|
disabled={selectedIds.size === 0}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 items-center justify-center">
|
||||||
|
<Printer size={18} color="black" />
|
||||||
|
<span className="text-[13px] font-medium">چاپ</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrintSectionNav
|
||||||
@@ -67,14 +67,31 @@ const ManageForms: FC<Props> = (props) => {
|
|||||||
<div className='text-sm mb-3'>{item.name}</div>
|
<div className='text-sm mb-3'>{item.name}</div>
|
||||||
<div className='flex gap-5 flex-wrap'>
|
<div className='flex gap-5 flex-wrap'>
|
||||||
{
|
{
|
||||||
item.options?.map((value) => {
|
item.options?.map((option) => {
|
||||||
|
const selectedValues = (value?.value ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((v) => v.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
const isChecked = selectedValues.includes(option.value)
|
||||||
|
|
||||||
|
const handleCheckboxChange = (checked: boolean) => {
|
||||||
|
if (item.multiple) {
|
||||||
|
const next = checked
|
||||||
|
? [...selectedValues, option.value]
|
||||||
|
: selectedValues.filter((v) => v !== option.value)
|
||||||
|
handleChange(item.id, next.join(','))
|
||||||
|
} else {
|
||||||
|
handleChange(item.id, checked ? option.value : '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={value.id} className='flex gap-2 items-center'>
|
<div key={option.id} className='flex gap-2 items-center'>
|
||||||
<div>{value.value}</div>
|
<div>{option.value}</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name={item.id + ''}
|
name={item.id + ''}
|
||||||
checked={value?.value === value.value}
|
checked={isChecked}
|
||||||
onCheckedChange={(checked) => handleChange(item.id, checked ? value.value : '')}
|
onCheckedChange={handleCheckboxChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type FC } from "react";
|
import { type FC } from "react";
|
||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes, useLocation } from "react-router-dom";
|
||||||
import Home from "../pages/home/Home";
|
import Home from "../pages/home/Home";
|
||||||
import SideBar from "../shared/Sidebar";
|
import SideBar from "../shared/Sidebar";
|
||||||
import Header from "../shared/Header";
|
import Header from "../shared/Header";
|
||||||
@@ -54,11 +54,23 @@ import UpdateRole from "@/pages/role/Update";
|
|||||||
import RequestDetail from "@/pages/requests/Detail";
|
import RequestDetail from "@/pages/requests/Detail";
|
||||||
import ConvertToOrders from "@/pages/order/ConvertToOrders";
|
import ConvertToOrders from "@/pages/order/ConvertToOrders";
|
||||||
import OrderPrint from "@/pages/order/Print";
|
import OrderPrint from "@/pages/order/Print";
|
||||||
|
import OrderPrintPreview from "@/pages/order/PrintPreview";
|
||||||
import TicketList from "@/pages/ticket/TicketList";
|
import TicketList from "@/pages/ticket/TicketList";
|
||||||
import TicketDetail from "@/pages/ticket/Detail";
|
import TicketDetail from "@/pages/ticket/Detail";
|
||||||
import PaymentsList from "@/pages/payment/List";
|
import PaymentsList from "@/pages/payment/List";
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname)
|
||||||
|
|
||||||
|
if (isPrintPreview) {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 overflow-hidden">
|
<div className="p-4 overflow-hidden">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -99,6 +111,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={Paths.order.create} element={<NewOrder />} />
|
<Route path={Paths.order.create} element={<NewOrder />} />
|
||||||
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
|
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
|
||||||
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
|
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
|
||||||
|
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
|
||||||
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
|
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
|
||||||
|
|
||||||
<Route path={Paths.tickets.list} element={<TicketList />} />
|
<Route path={Paths.tickets.list} element={<TicketList />} />
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const SideBar: FC = () => {
|
|||||||
title={t('sidebar.mainPage')}
|
title={t('sidebar.mainPage')}
|
||||||
isActive={isActive('/home')}
|
isActive={isActive('/home')}
|
||||||
link={Paths.home}
|
link={Paths.home}
|
||||||
activeName='dashboard'
|
activeName=''
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
@@ -123,7 +123,7 @@ const SideBar: FC = () => {
|
|||||||
title={'فرم ساز'}
|
title={'فرم ساز'}
|
||||||
isActive={isActive('print')}
|
isActive={isActive('print')}
|
||||||
link={Paths.print.list}
|
link={Paths.print.list}
|
||||||
activeName='view_print'
|
activeName='view_print_form'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SideBarItem
|
<SideBarItem
|
||||||
|
|||||||
Reference in New Issue
Block a user