This commit is contained in:
hamid zarghami
2025-05-24 15:16:30 +03:30
parent 35979729d3
commit 27d861e86a
9 changed files with 471 additions and 66 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

+146
View File
@@ -0,0 +1,146 @@
.invoice-container {
padding: 0;
margin: 0 auto;
max-width: 29.7cm;
-webkit-print-color-adjust: exact;
direction: rtl;
font: 9pt irancell;
}
.page {
background: white;
page-break-after: always;
padding: 0.5cm;
}
.flex {
display: flex;
}
.flex > * {
float: left;
}
.flex-grow {
flex-grow: 10000000;
}
.barcode {
text-align: center;
margin: 12px 0 0 0;
height: 30px;
}
.barcode span {
font-size: 35pt;
font-family: "Libre Barcode 128";
}
.portait {
transform: rotate(-90deg) translate(0, 40%);
text-align: center;
}
.header-item-wrapper {
border: 1px solid #000;
width: 100%;
height: 100%;
background: #eee;
display: flex;
align-content: center;
}
thead,
tfoot {
background: #eee;
}
.header-item-data {
height: 100%;
width: 100%;
}
.bordered {
border: 1px solid #000;
padding: 0.12cm;
}
.header-table {
width: 100%;
table-layout: fixed;
border-spacing: 0;
}
.header-table td {
padding: 0;
vertical-align: top;
}
.content-table {
border-collapse: collapse;
width: 100%;
table-layout: fixed;
border-spacing: 0;
}
.content-table td,
.content-table th {
border: 1px solid #000;
text-align: center;
padding: 0.1cm;
font-weight: normal;
}
.centered td {
vertical-align: middle;
}
.serials {
direction: ltr;
text-align: left;
}
.title {
text-align: right;
}
.grow {
width: 100%;
height: 100%;
}
.font-small {
font-size: 8pt;
}
.font-medium {
font-size: 10pt;
}
.font-big {
font-size: 15pt;
}
.label {
font-weight: bold;
padding: 0 0 0 2px;
}
.ltr {
direction: ltr;
display: block;
}
@media print {
.print-button {
display: none;
visibility: hidden;
}
}
@page {
size: A4 landscape;
margin: 0;
margin-bottom: 0.5cm;
margin-top: 0.5cm;
}
+278
View File
@@ -0,0 +1,278 @@
import { useEffect } from 'react';
import './Invoice.css';
import { useParams } from 'react-router-dom';
import { useGetInvoiceDetail } from '../pages/receipts/hooks/useReceiptData';
import { useGetProfile } from '../pages/profile/hooks/useProfileData';
import moment from 'moment-jalaali';
import { NumberFormat } from '../config/func';
import { ReceiptDetailItemType } from '../pages/receipts/types/ReceiptTypes';
import SignImage from '../assets/images/sign.png'
import { useTranslation } from 'react-i18next';
import Button from './Button';
import { Printer } from 'iconsax-react';
const Invoice = () => {
const { t } = useTranslation('global')
const { id } = useParams();
const getInvoice = useGetInvoiceDetail(id ? id : '');
const getProfile = useGetProfile();
useEffect(() => {
const handleResize = () => {
try {
const mvp = document.getElementById('vp');
if (!mvp) return;
if (screen.width >= 300 && screen.width < 500) {
mvp.setAttribute('content', 'initial-scale=0.35,width=device-width');
} else if (screen.width > 500 && screen.width < 600) {
mvp.setAttribute('content', 'initial-scale=0.6,width=device-width');
} else if (screen.width > 600 && screen.width < 700) {
mvp.setAttribute('content', 'initial-scale=0.7,width=device-width');
}
} catch (e) {
console.error('Error setting viewport:', e);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
const handlePrint = () => {
// Add print-specific styles before printing
const style = document.createElement('style');
style.innerHTML = `
@media print {
@page {
margin: 0;
}
body * {
visibility: hidden;
}
.invoice-container, .invoice-container * {
visibility: visible;
}
}
`;
document.head.appendChild(style);
window.print();
document.head.removeChild(style);
};
if (getInvoice.isPending || getProfile.isPending) {
return <div></div>;
}
const invoice = getInvoice.data?.data?.invoice;
const profile = getProfile.data?.data?.user;
return (
<div className="invoice-container">
<meta
id="vp"
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<Button
className='w-fit px-7 print-button mb-4'
onClick={handlePrint}
>
<div className='flex gap-2'>
<Printer size={20} color='white' />
<div>چاپ</div>
</div>
</Button>
<div className="page">
<h1 className='font-bold text-lg pb-4' style={{ textAlign: 'center' }}>
{invoice?.status === 'PAID' ? 'فاکتور (پرداخت شده)' : 'پیش فاکتور (پرداخت نشده)'}
</h1>
<table className="header-table" style={{ width: '100%' }}>
<tr>
<td style={{ width: '1.8cm', height: '2.5cm', verticalAlign: 'middle', paddingBottom: '4px' }}>
<div className="header-item-wrapper">
<div className="portait" style={{ margin: '5px' }}>فروشندده</div>
</div>
</td>
<td style={{ padding: '0 4px 4px', height: '2cm' }}>
<div className="bordered grow header-item-data">
<table className="grow centered">
<tr>
<td style={{ width: '7cm' }}>
<span className="label">فروشنده:</span> رسا نگاره داناک
</td>
<td style={{ width: '5cm' }}>
<span className="label">شناسه ملی:</span> ۱۴۰۰۵۴۵۳۱۹۰
</td>
<td>
<span className="label">شماره ثبت:</span> ۴۸۳۵۱۶
</td>
<td>
<span className="label">شماره اقتصادی:</span> ۱۴۰۰۵۴۵۳۱۹۰
</td>
</tr>
<tr>
<td colSpan={2}>
<span className="label">نشانی شرکت:</span>تهران - یوسف آباد - خیابان فتحی شقاقی - پلاک ۵ - واحد ۱۳
</td>
<td>
<span className="label">کدپستی:</span> ۱۴۳۳۶۳۴۴۴۶
</td>
<td>
<span className="label">تلفن و فکس:</span> ۰۲۱۹۱۶۹۳۵۳۳
</td>
</tr>
</table>
</div>
</td>
<td style={{ width: '7cm', height: '2cm', padding: '0 4px 4px 0' }}>
<div className="bordered" style={{ textAlign: 'center', height: '100%', padding: '0.4cm 0.2cm' }}>
<div className="flex">
<div className="font-small">شماره فاکتور:</div>
<div className="flex-grow" style={{ textAlign: 'left' }}>{moment(invoice?.createdAt).format('jYYYY') + invoice?.numericId}</div>
</div>
<div className="flex">
<div>تاریخ:</div>
<div className="flex-grow" style={{ textAlign: 'left' }}>{moment(invoice?.createdAt).format('jYYYY/jMM/jDD')}</div>
</div>
<div className="flex" style={{ marginBottom: '4px' }}>
<div>پیگیری:</div>
<div className="flex-grow font-medium" style={{ textAlign: 'left' }}>{invoice?.numericId}</div>
</div>
</div>
</td>
</tr>
<tr>
<td style={{ width: '1.8cm', height: '2.5cm', verticalAlign: 'middle', paddingBottom: '4px' }}>
<div className="header-item-wrapper">
<div className="portait" style={{ margin: '5px' }}>خریدار</div>
</div>
</td>
<td style={{ padding: '0 4px 4px', height: '2cm' }}>
<div className="bordered grow header-item-data">
<table className="grow centered">
<tr>
<td style={{ width: '7cm' }}>
<span className="label">خریدار:</span> {profile?.financialType === 'LEGAL' ? profile?.legalUser?.registrationName : `${profile?.firstName} ${profile?.lastName}`}
</td>
<td style={{ width: '5cm' }}>
<span className="label">شناسه ملی:</span> {profile?.nationalCode || '-'}
</td>
<td>
<span className="label">شماره ثبت:</span> {profile?.financialType === 'LEGAL' ? profile?.legalUser?.registrationCode : '-'}
</td>
<td>
<span className="label">شماره اقتصادی:</span> {profile?.financialType === 'LEGAL' ? profile?.legalUser?.economicCode : '-'}
</td>
</tr>
<tr>
<td colSpan={2}>
<span className="label">نشانی:</span> {profile?.financialType === 'LEGAL' ? profile?.legalUser?.address?.fullAddress : profile?.address?.fullAddress || '-'}
</td>
<td>
<span className="label">کدپستی:</span> {profile?.financialType === 'LEGAL' ? profile?.legalUser?.address?.postalCode : profile?.address?.postalCode || '-'}
</td>
<td>
<span className="label">تلفن:</span> {profile?.phone || '-'}
</td>
</tr>
</table>
</div>
</td>
<td style={{ width: '7cm', height: '2cm', padding: '0 4px 4px 0' }}>
<div className="bordered" style={{ textAlign: 'center', height: '100%', padding: '0.4cm 0.2cm' }}>
<div className="flex">
<div className="font-small">نوع شخص:</div>
<div className="flex-grow" style={{ textAlign: 'left' }}>{profile?.financialType === 'LEGAL' ? 'حقوقی' : 'حقیقی'}</div>
</div>
<div className="flex">
<div>وضعیت:</div>
<div className="flex-grow" style={{ textAlign: 'left' }}>{t(`receip.${invoice?.status}`)}</div>
</div>
<div className="flex" style={{ marginBottom: '4px' }}>
<div>تاریخ پرداخت:</div>
<div className="flex-grow font-medium" style={{ textAlign: 'left' }}>{invoice?.paidAt ? moment(invoice?.paidAt).format('jYYYY/jMM/jDD') : '-'}</div>
</div>
</div>
</td>
</tr>
</table>
<table className="content-table">
<thead>
<tr>
<th>ردیف</th>
<th>شناسه کالا یا خدمت</th>
<th style={{ width: '30%' }}>شرح کالا یا خدمت</th>
<th>آمر</th>
<th>تعداد</th>
<th style={{ width: '2.3cm' }}>مبلغ واحد (ریال)</th>
<th style={{ width: '2.3cm' }}>مبلغ کل (ریال)</th>
<th style={{ width: '2.3cm' }}>تخفیف (ریال)</th>
<th style={{ width: '2.3cm' }}>مبلغ کل پس از تخفیف (ریال)</th>
<th style={{ width: '2.3cm' }}>جمع مالیات و عوارض ارزش افزوده (ریال)</th>
<th style={{ width: '2.5cm' }}>جمع کل پس از تخفیف و مالیات و عوارض (ریال)</th>
</tr>
</thead>
<tbody>
{invoice?.items?.map((item: ReceiptDetailItemType, index: number) => (
<tr key={item.id}>
<td>{index + 1}</td>
<td>{item.id.slice(-5)}</td>
<td>{item.name}</td>
<td>داناک</td>
<td>{item.count}</td>
<td>{NumberFormat(Number(item.unitPrice))}</td>
<td>{NumberFormat(item.totalPrice)}</td>
<td>{NumberFormat(Number(item.discount))}</td>
<td>{NumberFormat(item.totalPrice - Number(item.discount))}</td>
<td>{NumberFormat(invoice.tax)}</td>
<td>{NumberFormat(invoice.totalPrice)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr>
<td colSpan={6}></td>
<td colSpan={4} className="font-small">
جمع کل پس از کسر تخفیف با احتساب مالیات و عوارض (ریال):
</td>
<td><span className="ltr">{NumberFormat(invoice?.totalPrice)}</span></td>
</tr>
<tr>
<td colSpan={4}>
اعتبار مالیاتی قابل استفاده توسط خریدار
</td>
<td colSpan={7} className="font-small">
</td>
</tr>
<tr style={{ background: '#fff' }}>
<td colSpan={11} style={{ height: '2.5cm', verticalAlign: 'top', padding: '10px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<div style={{ width: '40%', textAlign: 'right' }}>مهر و امضای فروشنده:</div>
<div style={{ width: '40%', textAlign: 'right' }}>تاریخ خرید:</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
<div style={{ width: '40%', display: 'flex', justifyContent: 'center' }}>
<img className="footer-img" width="150px" src={SignImage} />
</div>
<div style={{ width: '40%', textAlign: 'right', paddingTop: '10px' }}>
{moment(invoice?.paidAt).format('jYYYY/jMM/jDD')}
</div>
</div>
</div>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
);
};
export default Invoice;
+15 -53
View File
@@ -1,65 +1,27 @@
import { jsPDF } from 'jspdf';
import html2canvas from 'html2canvas';
import { FC } from 'react';
import Button from './Button';
import { useTranslation } from 'react-i18next';
import { useGetInvoiceDetail } from '../pages/receipts/hooks/useReceiptData';
import { useNavigate, useParams } from 'react-router-dom';
import { useGetProfile } from '../pages/profile/hooks/useProfileData';
import { Pages } from '../config/Pages';
interface PDFGeneratorProps {
elementId: string;
fileName: string;
}
const PDFGenerator: FC<PDFGeneratorProps> = ({ elementId, fileName }) => {
const PDFGenerator: FC = () => {
const { id } = useParams()
const { t } = useTranslation('global');
const generatePDF = async () => {
const element = document.getElementById(elementId);
if (!element) return;
// تنظیم استایل‌های موقت برای PDF
const originalStyles = element.getAttribute('style') || '';
element.setAttribute('style', `
${originalStyles}
width: 800px !important;
margin: 0 auto !important;
padding: 20px !important;
`);
const canvas = await html2canvas(element, {
scale: 1.5, // کاهش کیفیت برای سایز کمتر
useCORS: true,
logging: false,
backgroundColor: '#ffffff',
windowWidth: 800,
width: 800
});
// برگرداندن استایل‌های اصلی
element.setAttribute('style', originalStyles);
const imgData = canvas.toDataURL('image/jpeg', 0.8); // استفاده از JPEG با کیفیت 80%
const pdf = new jsPDF({
orientation: 'portrait',
unit: 'mm',
format: 'a4'
});
const imgWidth = 180; // کاهش عرض برای حاشیه بیشتر
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// محاسبه موقعیت برای قرار دادن در وسط صفحه
const x = (210 - imgWidth) / 2; // 210 عرض A4 در میلی‌متر
const y = (297 - imgHeight) / 2; // 297 ارتفاع A4 در میلی‌متر
pdf.addImage(imgData, 'JPEG', x, y, imgWidth, imgHeight);
pdf.save(`${fileName}.pdf`);
};
const navigate = useNavigate()
const getInvoce = useGetInvoiceDetail(id ? id : '')
const getProfile = useGetProfile()
const status = getInvoce.data?.data?.invoice?.status
const financialType = getProfile.data?.data?.user?.financialType
return (
<Button
label={t('receip.get_factor')}
className='bg-[#E8E8E8] text-description text-xs'
onClick={generatePDF}
onClick={() => navigate(Pages.receipts.factor + id)}
label={status === 'PAID' ? t('receip.get_factor') : t('receip.get_pre_factor')}
className='bg-[#000] text-xs mt-2'
disabled={financialType !== 'LEGAL' && financialType !== 'REAL'}
/>
);
};
+1
View File
@@ -15,6 +15,7 @@ export const Pages = {
receipts: {
index: "/receipts",
detail: "/receipts/",
factor: "/receipts/factor/",
},
ticket: {
list: "/tickets",
+3 -2
View File
@@ -250,7 +250,7 @@
"fullName": "نام و نام خانوادگی",
"compelete_financal_info_error": "برای دریافت فاکتور رسمی اطلاعات مالی خود را وارد کنید",
"compelete_financal_info": "تکمیل اطلاعات مالی",
"get_factor": "دریافت پیش فاکتور",
"get_pre_factor": "دریافت پیش فاکتور",
"pay": "پرداخت",
"confrim_factor": "تایید فاکتور",
"sure_confrim_factor": "برای تایید فاکتور مطمئن هستید؟",
@@ -266,7 +266,8 @@
"discount_removed": "کد تخفیف با موفقیت حذف شد",
"remove_discount": "حذف تخفیف",
"charge_wallet": "شارژ کیف پول",
"sure_charge_wallet": "آیا از شارژ کیف پول اطمینان دارید؟"
"sure_charge_wallet": "آیا از شارژ کیف پول اطمینان دارید؟",
"get_factor": "دریافت فاکتور "
},
"active": "فعال",
"inactive": "غیرفعال",
+4 -3
View File
@@ -296,11 +296,10 @@ const ReceiptsDetail: FC = () => {
</div>
{
getInvoce.data?.data?.invoice?.status === 'WAIT_PAYMENT' &&
getInvoce.data?.data?.invoice?.status === 'WAIT_PAYMENT' ?
<div className='flex gap-4 items-center mt-8'>
<PDFGenerator
elementId="receipt-details"
fileName={`receipt-${getInvoce.data?.data?.invoice.numericId}`}
/>
<PayInvoice
id={id ? id : ''}
@@ -308,6 +307,8 @@ const ReceiptsDetail: FC = () => {
gateWayId={gateWayId}
/>
</div>
: <PDFGenerator
/>
}
{
getInvoce.data?.data?.remainingToCharge > 0 &&
+14
View File
@@ -0,0 +1,14 @@
import { FC } from 'react'
import InvoiceComponent from '../../components/Invoice'
const Invoice: FC = () => {
return (
<div>
<InvoiceComponent />
</div>
)
}
export default Invoice
+2
View File
@@ -31,6 +31,7 @@ import LearningDetail from '../pages/learning/Detail'
import BuyService from '../pages/service/BuyService'
import Support from '../pages/support/Support'
import SupportInfo from '../pages/support/SupportInfo'
import Invoice from '../pages/receipts/Invoice'
const MainRouter: FC = () => {
return (
@@ -54,6 +55,7 @@ const MainRouter: FC = () => {
<Route path={Pages.transactions} element={<TransactionList />} />
<Route path={Pages.receipts.index} element={<ReceiptsList />} />
<Route path={Pages.receipts.detail + ':id'} element={<ReceiptsDetail />} />
<Route path={Pages.receipts.factor + ':id'} element={<Invoice />} />
<Route path={Pages.announcement.list} element={<AnnouncementtList />} />
<Route path={Pages.announcement.detail + ':id'} element={<AnnouncementDetail />} />
<Route path={Pages.criticisms} element={<AddCriticisms />} />