From 1e8579bbfb24ed71e7da24be5d643cbf6301d81b Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Fri, 3 Jul 2026 16:30:25 +0330 Subject: [PATCH] print form --- src/pages/auth/components/LoginStep1.tsx | 7 +- src/pages/auth/components/LoginStep2.tsx | 14 +- src/pages/auth/store/AuthStore.ts | 4 + src/pages/auth/types/AuthTypes.ts | 3 + src/pages/order/Print.tsx | 141 ++++++++++++---- src/pages/order/PrintPreview.tsx | 150 ++++++++++++++++++ .../order/components/PrintFieldDisplay.tsx | 44 +++++ .../order/components/PrintSectionCard.tsx | 32 ++++ .../order/components/PrintSectionNav.tsx | 81 ++++++++++ src/pages/print/components/ManageForms.tsx | 27 +++- src/router/MainRouter.tsx | 15 +- src/shared/Sidebar.tsx | 4 +- 12 files changed, 477 insertions(+), 45 deletions(-) create mode 100644 src/pages/order/PrintPreview.tsx create mode 100644 src/pages/order/components/PrintFieldDisplay.tsx create mode 100644 src/pages/order/components/PrintSectionCard.tsx create mode 100644 src/pages/order/components/PrintSectionNav.tsx diff --git a/src/pages/auth/components/LoginStep1.tsx b/src/pages/auth/components/LoginStep1.tsx index 1904586..1b2b396 100644 --- a/src/pages/auth/components/LoginStep1.tsx +++ b/src/pages/auth/components/LoginStep1.tsx @@ -12,7 +12,7 @@ import { extractErrorMessage } from '@/config/func' const LoginStep1: FC = () => { - const { phone, setPhone, setStepLogin } = useAuthStore() + const { phone, setPhone, setStepLogin, setDevOtpCode } = useAuthStore() const loginWithOtp = useLoginWithOtp() const checkHasAccount = useCheckHasAccount() @@ -27,7 +27,10 @@ const LoginStep1: FC = () => { onSubmit(values) { setPhone(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) toast('کد تایید ارسال شد', 'success') }, diff --git a/src/pages/auth/components/LoginStep2.tsx b/src/pages/auth/components/LoginStep2.tsx index 65b6b2e..5fbe573 100644 --- a/src/pages/auth/components/LoginStep2.tsx +++ b/src/pages/auth/components/LoginStep2.tsx @@ -15,7 +15,7 @@ import { Paths } from '@/config/Paths' const LoginStep2: FC = () => { - const { phone, setStepLogin } = useAuthStore() + const { phone, setStepLogin, devOtpCode, setDevOtpCode } = useAuthStore() const { value, reset } = useCountDown(2, true) const otpVerify = useOtpVerify() const loginWithOtp = useLoginWithOtp() @@ -24,7 +24,8 @@ const LoginStep2: FC = () => { const formik = useFormik({ initialValues: { phone: phone, - otp: '' + // TODO: remove before production — dev-only OTP autofill + otp: devOtpCode, }, validationSchema: Yup.object({ otp: Yup.string() @@ -81,7 +82,14 @@ const LoginStep2: FC = () => { const reSend = () => { 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() toast('کد مجدد ارسال شد', 'success') }, diff --git a/src/pages/auth/store/AuthStore.ts b/src/pages/auth/store/AuthStore.ts index c6751b4..99d2418 100644 --- a/src/pages/auth/store/AuthStore.ts +++ b/src/pages/auth/store/AuthStore.ts @@ -14,4 +14,8 @@ export const useAuthStore = create((set) => ({ setStepLogin(value) { set({ stepLogin: value }); }, + devOtpCode: "", + setDevOtpCode(value) { + set({ devOtpCode: value }); + }, })); diff --git a/src/pages/auth/types/AuthTypes.ts b/src/pages/auth/types/AuthTypes.ts index dc3b6fe..e7ae709 100644 --- a/src/pages/auth/types/AuthTypes.ts +++ b/src/pages/auth/types/AuthTypes.ts @@ -12,6 +12,9 @@ export type AuthStoreType = { setEmail: (value: string) => void; stepLogin: number; setStepLogin: (value: number) => void; + /** TODO: remove before production — dev-only OTP from API response */ + devOtpCode: string; + setDevOtpCode: (value: string) => void; }; export type LoginWithPasswordType = { diff --git a/src/pages/order/Print.tsx b/src/pages/order/Print.tsx index 0991f1f..1334301 100644 --- a/src/pages/order/Print.tsx +++ b/src/pages/order/Print.tsx @@ -1,21 +1,29 @@ -import { type FC } from 'react' -import { useParams, useNavigate } from 'react-router-dom' +import { useState, type FC } from 'react' +import { useParams } from 'react-router-dom' import { useFormik } from 'formik' import { useGetSections } from '../print/hooks/usePrintData' import { useGetOrderPrint, useSubmitOrderPrint } from './hooks/useOrderData' 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 BackButton from '@/components/BackButton' +import RefreshButton from '@/components/RefreshButton' import { TickSquare } from 'iconsax-react' import { toast } from '@/shared/toast' import { extractErrorMessage } from '@/config/func' +import { Paths } from '@/config/Paths' const OrderPrint: FC = () => { const { id: orderId } = useParams() - const navigate = useNavigate() - const { data } = useGetSections() + const { data, refetch: refetchSections, isFetching: isSectionsFetching } = useGetSections() 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>(() => new Set()) const formik = useFormik({ initialValues: { @@ -29,7 +37,6 @@ const OrderPrint: FC = () => { { onSuccess: () => { toast('با موفقیت ذخیره شد', 'success') - navigate(-1) }, onError: (error) => { toast(extractErrorMessage(error), 'error') @@ -39,35 +46,105 @@ 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 ( -
-
-

فرم چاپ سفارش

- -
-
- {data?.data?.map((section) => ( -
+
+
+ +

فرم چاپ سفارش

+
+
+ +
+ +
+
+ +
+
+ {sections.map((section) => ( + -
- ))} + ))} +
+ + {sections.length > 0 && ( + + )}
) diff --git a/src/pages/order/PrintPreview.tsx b/src/pages/order/PrintPreview.tsx new file mode 100644 index 0000000..9ff998b --- /dev/null +++ b/src/pages/order/PrintPreview.tsx @@ -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 ( +
+

بخشی برای چاپ انتخاب نشده است.

+
+ ) + } + + return ( +
+
+ + +
+ +
+
+

فرم چاپ سفارش

+

+ {new Date().toLocaleDateString('fa-IR')} +

+
+ +
+ {sections.map((section) => ( +
+
+

+ {section.title} +

+
+
+ +
+
+ ))} +
+ +
+ نگاره — فرم چاپ سفارش +
+
+ + +
+ ) +} + +export { ORDER_PRINT_STORAGE_KEY } +export default OrderPrintPreview diff --git a/src/pages/order/components/PrintFieldDisplay.tsx b/src/pages/order/components/PrintFieldDisplay.tsx new file mode 100644 index 0000000..1b89f85 --- /dev/null +++ b/src/pages/order/components/PrintFieldDisplay.tsx @@ -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 = ({ fields, values }) => { + const getValue = (fieldId: string) => + values.find((v) => v.fieldId === fieldId)?.value ?? '' + + return ( +
+ {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 ( +
+
{field.name}
+
+ {displayValue} +
+
+ ) + })} +
+ ) +} + +export default PrintFieldDisplay diff --git a/src/pages/order/components/PrintSectionCard.tsx b/src/pages/order/components/PrintSectionCard.tsx new file mode 100644 index 0000000..6125bba --- /dev/null +++ b/src/pages/order/components/PrintSectionCard.tsx @@ -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 +} + +const PrintSectionCard: FC = ({ title, fields, formik }) => { + return ( +
+
+

+ {title} +

+
+
+ +
+
+ ) +} + +export default PrintSectionCard diff --git a/src/pages/order/components/PrintSectionNav.tsx b/src/pages/order/components/PrintSectionNav.tsx new file mode 100644 index 0000000..6a064cc --- /dev/null +++ b/src/pages/order/components/PrintSectionNav.tsx @@ -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 + onToggle: (id: string) => void + onToggleAll: (checked: boolean) => void + onSelectFilled: () => void + onPrint: () => void +} + +const PrintSectionNav: FC = ({ + 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 ( + + ) +} + +export default PrintSectionNav diff --git a/src/pages/print/components/ManageForms.tsx b/src/pages/print/components/ManageForms.tsx index 2792c7b..00148f7 100644 --- a/src/pages/print/components/ManageForms.tsx +++ b/src/pages/print/components/ManageForms.tsx @@ -67,14 +67,31 @@ const ManageForms: FC = (props) => {
{item.name}
{ - 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 ( -
-
{value.value}
+
+
{option.value}
handleChange(item.id, checked ? value.value : '')} + checked={isChecked} + onCheckedChange={handleCheckboxChange} />
) diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx index 36d3d00..4ec3d12 100644 --- a/src/router/MainRouter.tsx +++ b/src/router/MainRouter.tsx @@ -1,5 +1,5 @@ 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 SideBar from "../shared/Sidebar"; import Header from "../shared/Header"; @@ -54,11 +54,23 @@ import UpdateRole from "@/pages/role/Update"; import RequestDetail from "@/pages/requests/Detail"; import ConvertToOrders from "@/pages/order/ConvertToOrders"; import OrderPrint from "@/pages/order/Print"; +import OrderPrintPreview from "@/pages/order/PrintPreview"; import TicketList from "@/pages/ticket/TicketList"; import TicketDetail from "@/pages/ticket/Detail"; import PaymentsList from "@/pages/payment/List"; const MainRouter: FC = () => { + const location = useLocation() + const isPrintPreview = /\/order\/print\/[^/]+\/preview/.test(location.pathname) + + if (isPrintPreview) { + return ( + + } /> + + ) + } + return (
@@ -99,6 +111,7 @@ const MainRouter: FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/src/shared/Sidebar.tsx b/src/shared/Sidebar.tsx index 5c641f0..ae1251b 100644 --- a/src/shared/Sidebar.tsx +++ b/src/shared/Sidebar.tsx @@ -67,7 +67,7 @@ const SideBar: FC = () => { title={t('sidebar.mainPage')} isActive={isActive('/home')} link={Paths.home} - activeName='dashboard' + activeName='' /> { title={'فرم ساز'} isActive={isActive('print')} link={Paths.print.list} - activeName='view_print' + activeName='view_print_form' />