mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-208): add PurchaseOrderInvoice component for PDF generation and update PurchaseOrderDetail to integrate invoice display
This commit is contained in:
@@ -14,6 +14,7 @@ import Modal from '@/components/Modal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import PurchaseOrderStaffApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm';
|
||||
import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm';
|
||||
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
|
||||
|
||||
import { BaseGroupedApproval } from '@/types/api/api-general';
|
||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||
@@ -49,17 +50,17 @@ const dummyPurchaseData: Purchase = {
|
||||
supplier: {
|
||||
id: 1,
|
||||
name: 'PT. CHAROEN POKPHAND JAYA FARM',
|
||||
alias: '',
|
||||
pic: '',
|
||||
type: '',
|
||||
category: '',
|
||||
hatchery: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '-',
|
||||
npwp: '',
|
||||
account_number: '',
|
||||
due_date: 0,
|
||||
alias: 'CP JAYA FARM',
|
||||
pic: 'Budi Santoso',
|
||||
type: 'Supplier',
|
||||
category: 'Feed',
|
||||
hatchery: 'Jawa Barat',
|
||||
phone: '+62-22-7563850',
|
||||
email: 'info@cp.co.id',
|
||||
address: 'Jl. Raya Bandung - Sumedang Km. 28, Desa Cisantana, Kec. Cigendel, Kabupaten Sumedang, Jawa Barat 45363',
|
||||
npwp: '01.938.451.6-433.000',
|
||||
account_number: '123-456-7890',
|
||||
due_date: 30,
|
||||
balance: 0,
|
||||
created_at: '2025-01-01T00:00:00Z',
|
||||
updated_at: '2025-01-01T00:00:00Z',
|
||||
@@ -682,27 +683,7 @@ const PurchaseOrderDetail = ({
|
||||
<div className='ml-3'>
|
||||
{purchaseData.po_number &&
|
||||
purchaseData.po_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
disabled={!purchaseData.po_document_path}
|
||||
href={purchaseData.po_document_path ?? undefined}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
{purchaseData.po_document_path ? (
|
||||
<>
|
||||
<Icon
|
||||
icon='material-symbols:file-open-outline'
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{purchaseData.po_number}
|
||||
</>
|
||||
) : (
|
||||
purchaseData.po_number
|
||||
)}
|
||||
</Button>
|
||||
<PurchaseOrderInvoice data={purchaseData} />
|
||||
) : (
|
||||
<Button
|
||||
color='primary'
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
},
|
||||
address: {
|
||||
fontSize: 8,
|
||||
color: '#666666',
|
||||
maxWidth: 300,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: '#ccc',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
padding: 4,
|
||||
fontSize: 8,
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
interface PurchaseOrderInvoiceProps {
|
||||
data?: Purchase;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
const [, setIsGeneratingPDF] = useState(false);
|
||||
const purchaseData = data;
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
purchaseData?.items?.reduce(
|
||||
(sum, item) => sum + (item.total_price || 0),
|
||||
0
|
||||
) || 0
|
||||
);
|
||||
}, [purchaseData?.items]);
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!purchaseData) {
|
||||
alert('No purchase order data available');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const PDFDocument = () => (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>PT MITRA BERLIAN UNGGAS</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Purchase Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>PURCHASE ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>PO Number: {purchaseData?.po_number || '-'}</Text>
|
||||
<Text>
|
||||
Date:{' '}
|
||||
{purchaseData?.po_date
|
||||
? formatDate(purchaseData.po_date, 'DD-MMM-YYYY')
|
||||
: formatDate(new Date(), 'DD-MMM-YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Vendor and Ship To Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Vendor</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Ship To</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{purchaseData?.supplier?.name || '-'}
|
||||
</Text>
|
||||
<Text>{purchaseData?.supplier?.pic || '-'}</Text>
|
||||
<Text>
|
||||
{purchaseData?.supplier?.phone || '-'} /{' '}
|
||||
{purchaseData?.supplier?.email || '-'}
|
||||
</Text>
|
||||
<Text>{purchaseData?.supplier?.address || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
PT MITRA BERLIAN UNGGAS
|
||||
</Text>
|
||||
<Text>{purchaseData?.location?.name || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Item Description Table */}
|
||||
<View>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Total Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{purchaseData?.items?.map((item, index) => {
|
||||
const isLastItem =
|
||||
index === (purchaseData?.items?.length || 0) - 1;
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
isLastItem ? {} : pdfStyles.tableBorderBottom,
|
||||
]}
|
||||
>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp.{formatNumber(item.price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.total_qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp.{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View style={[pdfStyles.tableCellRightLast, { fontWeight: 'bold' }]}>
|
||||
<Text>Rp.{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Product Allocation Section */}
|
||||
<View style={pdfStyles.allocationSection}>
|
||||
<Text style={pdfStyles.sectionTitle}>Product Allocation</Text>
|
||||
<View style={pdfStyles.allocationTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Warehouse Name</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>PIC</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Address Detail</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Product Allocation</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{purchaseData?.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{purchaseData?.created_user?.name || '-'}</Text>
|
||||
<Text style={{ fontSize: 8 }}>
|
||||
{purchaseData?.created_user?.email || '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{purchaseData?.location?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
{/* Inner table for product allocation */}
|
||||
<View style={pdfStyles.innerTable}>
|
||||
{purchaseData?.items?.map((item, index) => (
|
||||
<View key={index} style={pdfStyles.innerRow}>
|
||||
<Text style={pdfStyles.innerCell}>
|
||||
{item.product?.name || '-'}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
pdfStyles.innerCell,
|
||||
{ textAlign: 'right' },
|
||||
]}
|
||||
>
|
||||
{formatNumber(item.total_qty || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
)) || []}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Special Instruction</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>{purchaseData?.notes || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT MITRA BERLIAN UNGGAS</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
const blob = await pdf(<PDFDocument />).toBlob();
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${purchaseData?.po_number || 'purchase-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!purchaseData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No purchase order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return purchaseData?.po_number &&
|
||||
purchaseData.po_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{purchaseData.po_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default PurchaseOrderInvoice;
|
||||
Reference in New Issue
Block a user