print form
deploy to danak / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-07-03 16:30:25 +03:30
parent c5deae8264
commit 1e8579bbfb
12 changed files with 477 additions and 45 deletions
+5 -2
View File
@@ -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')
},
+11 -3
View File
@@ -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<OtpVerifyType>({
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')
},
+4
View File
@@ -14,4 +14,8 @@ export const useAuthStore = create<AuthStoreType>((set) => ({
setStepLogin(value) {
set({ stepLogin: value });
},
devOtpCode: "",
setDevOtpCode(value) {
set({ devOtpCode: value });
},
}));
+3
View File
@@ -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 = {
+103 -26
View File
@@ -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<Set<string>>(() => new Set())
const formik = useFormik<OrderPrintFormType>({
initialValues: {
@@ -29,7 +37,6 @@ const OrderPrint: FC = () => {
{
onSuccess: () => {
toast('با موفقیت ذخیره شد', 'success')
navigate(-1)
},
onError: (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 (
<div className='mt-5'>
<div className='flex justify-between items-center'>
<h1 className='text-lg font-light'>فرم چاپ سفارش</h1>
<div className="mt-5">
<div className="flex justify-between items-center">
<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
className='w-fit px-6'
className="w-fit px-6"
isLoading={isPending}
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-1.5'>
<TickSquare size={18} color='black' />
<div className='text-[13px] font-light'>ذخیره</div>
<div className="flex gap-1.5">
<TickSquare size={18} color="black" />
<div className="text-[13px] font-light">ذخیره</div>
</div>
</Button>
</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 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>
{sections.length > 0 && (
<PrintSectionNav
sections={sections}
selectedIds={selectedSections}
onToggle={toggleSection}
onToggleAll={toggleAll}
onSelectFilled={selectFilledSections}
onPrint={handlePrint}
/>
)}
</div>
</div>
)
}
+150
View File
@@ -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
+22 -5
View File
@@ -67,14 +67,31 @@ const ManageForms: FC<Props> = (props) => {
<div className='text-sm mb-3'>{item.name}</div>
<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 (
<div key={value.id} className='flex gap-2 items-center'>
<div>{value.value}</div>
<div key={option.id} className='flex gap-2 items-center'>
<div>{option.value}</div>
<Checkbox
name={item.id + ''}
checked={value?.value === value.value}
onCheckedChange={(checked) => handleChange(item.id, checked ? value.value : '')}
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
</div>
)
+14 -1
View File
@@ -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 (
<Routes>
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
</Routes>
)
}
return (
<div className="p-4 overflow-hidden">
<Header />
@@ -99,6 +111,7 @@ const MainRouter: FC = () => {
<Route path={Paths.order.create} element={<NewOrder />} />
<Route path={Paths.order.edit + ":id"} element={<EditOrder />} />
<Route path={Paths.convertToOrder} element={<ConvertToOrders />} />
<Route path={Paths.orderPrint + ':id/preview'} element={<OrderPrintPreview />} />
<Route path={Paths.orderPrint + ':id'} element={<OrderPrint />} />
<Route path={Paths.tickets.list} element={<TicketList />} />
+2 -2
View File
@@ -67,7 +67,7 @@ const SideBar: FC = () => {
title={t('sidebar.mainPage')}
isActive={isActive('/home')}
link={Paths.home}
activeName='dashboard'
activeName=''
/>
<SideBarItem
@@ -123,7 +123,7 @@ const SideBar: FC = () => {
title={'فرم ساز'}
isActive={isActive('print')}
link={Paths.print.list}
activeName='view_print'
activeName='view_print_form'
/>
<SideBarItem