mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
538 lines
16 KiB
TypeScript
538 lines
16 KiB
TypeScript
'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 toast from 'react-hot-toast';
|
|
|
|
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: 30,
|
|
height: 30,
|
|
marginBottom: 8,
|
|
},
|
|
companyInfo: {
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
marginBottom: 4,
|
|
color: '#1f74bf',
|
|
},
|
|
address: {
|
|
fontSize: 8,
|
|
color: '#666666',
|
|
maxWidth: 400,
|
|
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,
|
|
color: '#1f74bf',
|
|
},
|
|
poInfo: {
|
|
flex: 1,
|
|
fontSize: 9,
|
|
textAlign: 'right',
|
|
},
|
|
sectionTitle: {
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
marginBottom: 8,
|
|
color: '#1f74bf',
|
|
},
|
|
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,
|
|
borderWidth: 1,
|
|
borderColor: '#000000',
|
|
},
|
|
innerRow: {
|
|
flexDirection: 'row',
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: '#000000',
|
|
borderBottomStyle: 'solid',
|
|
},
|
|
innerCell: {
|
|
flex: 1,
|
|
borderRightWidth: 1,
|
|
borderRightColor: '#000000',
|
|
borderRightStyle: 'solid',
|
|
padding: 8,
|
|
fontSize: 9,
|
|
},
|
|
innerCellLast: {
|
|
flex: 1,
|
|
padding: 8,
|
|
fontSize: 9,
|
|
},
|
|
innerCellRight: {
|
|
flex: 1,
|
|
borderRightWidth: 1,
|
|
borderRightColor: '#000000',
|
|
borderRightStyle: 'solid',
|
|
padding: 8,
|
|
fontSize: 9,
|
|
textAlign: 'right',
|
|
},
|
|
innerCellRightLast: {
|
|
flex: 1,
|
|
padding: 8,
|
|
fontSize: 9,
|
|
textAlign: 'right',
|
|
},
|
|
footer: {
|
|
marginTop: 30,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
},
|
|
footerCompany: {
|
|
fontSize: 12,
|
|
fontWeight: 'bold',
|
|
textAlign: 'right',
|
|
flex: 1,
|
|
color: '#1f74bf',
|
|
},
|
|
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) {
|
|
toast.error('No purchase order data available');
|
|
return;
|
|
}
|
|
|
|
setIsGeneratingPDF(true);
|
|
try {
|
|
const PDFDocument = () => (
|
|
<Document>
|
|
<Page size='A4' style={pdfStyles.page}>
|
|
{/* Header Section */}
|
|
<View style={pdfStyles.header}>
|
|
{/* eslint-disable-next-line jsx-a11y/alt-text */}
|
|
<Image
|
|
src='/assets/img/lti-logo.png'
|
|
style={pdfStyles.logo}
|
|
id={'mbu-logo'}
|
|
/>
|
|
<Text style={pdfStyles.companyInfo}>
|
|
PT LUMBUNG TELUR INDONESIA
|
|
</Text>
|
|
<Text style={pdfStyles.address}>
|
|
Setra Duta Raya No.L3 No.7, Ciwaruga, Kec. Parongpong, Kabupaten
|
|
Bandung Barat, Jawa Barat 40514
|
|
</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 || '-'} (
|
|
{purchaseData?.supplier?.alias || ''})
|
|
</Text>
|
|
<Text>{purchaseData?.supplier?.category || '-'}</Text>
|
|
<Text>
|
|
Credit Term: {purchaseData?.credit_term || 0} hari
|
|
</Text>
|
|
<Text>
|
|
Due Date:{' '}
|
|
{purchaseData?.due_date
|
|
? formatDate(purchaseData.due_date, 'DD MMM YYYY')
|
|
: '-'}
|
|
</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCellLast}>
|
|
<Text style={{ fontWeight: 'bold' }}>
|
|
PT LUMBUNG TELUR INDONESIA
|
|
</Text>
|
|
<Text>
|
|
{purchaseData?.items?.[0]?.warehouse &&
|
|
'location' in purchaseData.items[0].warehouse
|
|
? purchaseData.items[0].warehouse.location.name
|
|
: '-'}
|
|
</Text>
|
|
<Text>
|
|
{purchaseData?.items?.[0]?.warehouse &&
|
|
'location' in purchaseData.items[0].warehouse
|
|
? purchaseData.items[0].warehouse.location.address
|
|
: '-'}
|
|
</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>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.sub_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>Area</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCellHeader}>
|
|
<Text>Location Address</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCellHeaderLast}>
|
|
<Text>Product Allocation</Text>
|
|
</View>
|
|
</View>
|
|
{purchaseData?.items?.map((item, itemIndex) => (
|
|
<View key={itemIndex} style={pdfStyles.tableRow}>
|
|
<View style={pdfStyles.tableCell}>
|
|
<Text>{item.warehouse?.name || '-'}</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCell}>
|
|
<Text>{item.warehouse?.area?.name || '-'}</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCell}>
|
|
<Text>
|
|
{item.warehouse && 'location' in item.warehouse
|
|
? item.warehouse.location.address
|
|
: '-'}
|
|
</Text>
|
|
</View>
|
|
<View style={pdfStyles.tableCellLast}>
|
|
{/* Inner table for product allocation */}
|
|
<View style={pdfStyles.innerTable}>
|
|
{/* Header for inner table */}
|
|
<View
|
|
style={[
|
|
pdfStyles.innerRow,
|
|
{ backgroundColor: '#F5F5F5' },
|
|
]}
|
|
>
|
|
<Text style={pdfStyles.innerCell}>Item</Text>
|
|
<Text style={pdfStyles.innerCellRightLast}>
|
|
Quantity
|
|
</Text>
|
|
</View>
|
|
{/* Data row */}
|
|
<View style={pdfStyles.innerRow}>
|
|
<Text style={pdfStyles.innerCell}>
|
|
{item.product?.name || '-'}
|
|
</Text>
|
|
<Text style={pdfStyles.innerCellRightLast}>
|
|
{formatNumber(item.total_qty || 0)} of{' '}
|
|
{formatNumber(item.sub_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>Notes</Text>
|
|
</View>
|
|
</View>
|
|
<View style={pdfStyles.tableRow}>
|
|
<View style={pdfStyles.tableCellLast}>
|
|
<Text>{purchaseData?.notes || '-'}</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
<View style={pdfStyles.footerCompany}>
|
|
<Text>PT LUMBUNG TELUR INDONESIA</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 {
|
|
toast.error('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;
|