From 0cc01ae738159d088e53bb1cebc88a13ccbf6186 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Fri, 28 Nov 2025 10:31:27 +0700 Subject: [PATCH] feat(FE-196,199): create ExpensePDF component --- .../pages/expense/pdf/ExpensePDF.tsx | 651 ++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 src/components/pages/expense/pdf/ExpensePDF.tsx diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx new file mode 100644 index 00000000..664ecee1 --- /dev/null +++ b/src/components/pages/expense/pdf/ExpensePDF.tsx @@ -0,0 +1,651 @@ +'use client'; + +import { + Document, + Image, + Link, + Page, + StyleSheet, + Text, + View, +} from '@react-pdf/renderer'; + +import { Expense } from '@/types/api/expense'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; + +interface ExpensePDFProps { + expense?: Expense; +} + +const ExpensePDFStyle = StyleSheet.create({ + page: { + paddingTop: 24, + paddingBottom: 64, + paddingHorizontal: 32, + }, + + companyInfoHeader: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + companyLogo: { + width: 64, + height: 'auto', + }, + companyInfoHeaderDate: { + paddingTop: 8, + fontSize: 12, + }, + companyName: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + }, + companyAddress: { + fontSize: 8, + maxWidth: 400, + marginBottom: 10, + }, + + title: { + marginTop: 16, + fontSize: 16, + lineHeight: '150%', + textAlign: 'center', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + }, + + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 32, + + position: 'absolute', + fontSize: 10, + bottom: 30, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, + + // wrapper + generalInfoTable: { + width: '100%', + marginTop: 8, + borderWidth: 1, + borderColor: '#000000', + borderBottomWidth: 0, + fontSize: 12, + }, + + generalInfoTableRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000000', + }, + + // columns + generalInfoTableColLabel: { + width: '30%', + paddingVertical: 6, + paddingHorizontal: 8, + }, + generalInfoTableColSeparator: { + width: '3%', + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 6, + }, + generalInfoTableColValue: { + width: '67%', + paddingVertical: 6, + paddingHorizontal: 8, + }, + + generalInfoTableLabelText: { + fontWeight: 'bold', + }, + generalInfoTableValueText: {}, + + // expense detail table + expenseDetailContainer: { + width: '100%', + marginTop: 12, + }, + expenseDetailTitle: { + fontSize: 14, + lineHeight: '150%', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + textAlign: 'center', + }, + kandangExpenseContainer: { + width: '100%', + marginTop: 8, + }, + kandangExpenseTitle: { + fontSize: 14, + lineHeight: '150%', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + textAlign: 'center', + }, + kandangExpenseTable: { + width: '100%', + marginTop: 8, + borderWidth: 1, + borderColor: '#000000', + borderBottomWidth: 0, + fontSize: 12, + }, + kandangExpenseTableRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000000', + }, + kandangExpenseTableColLabel: { + width: '20%', + paddingVertical: 6, + paddingHorizontal: 8, + }, + kandangExpenseTableColLabelBorderRight: { + borderRight: '1px solid #000000', + }, + kandangExpenseTableColNonstock: { + width: '20%', + }, + kandangExpenseTableColNote: { + width: '40%', + }, + kandangExpenseHeaderLabelText: { + fontWeight: 'bold', + }, + kandangExpenseLabelText: { + fontSize: 10, + }, + kandangExpenseTableFooterColTotalExpenseCaption: { + width: '40%', + paddingVertical: 6, + paddingHorizontal: 8, + textAlign: 'right', + }, + kandangExpenseTableFooterColTotalExpenseValue: { + width: '60%', + paddingVertical: 6, + paddingHorizontal: 8, + }, + + // utils + doubleDivider: { + width: '100%', + height: 6, + borderTop: '2px solid black', + borderBottom: '2px solid black', + }, +}); + +const ExpensePDF = ({ expense }: ExpensePDFProps) => { + const isLatestApprovalRejected = + expense?.latest_approval?.action === 'REJECTED'; + const isExpenseRealized = + expense?.latest_approval?.step_number && + expense?.latest_approval.step_number >= 4; + + const realizationStatus = isExpenseRealized + ? 'Sudah Realisasi' + : 'Belum Realisasi'; + + const rows = [ + { label: 'Nomor PO', value: expense?.po_number }, + { label: 'Nomor Referensi', value: expense?.reference_number }, + { + label: 'Kategori', + value: + expense?.category === 'BOP' + ? 'Biaya Operasional' + : expense?.category === 'NON-BOP' + ? 'Non Biaya Operasional' + : '', + }, + { label: 'Lokasi', value: expense?.location.name }, + { + label: 'Kandang', + value: expense?.kandangs.map((item) => item.name).join(', '), + }, + { label: 'Vendor', value: expense?.supplier.name }, + { + label: 'Tanggal Transaksi', + value: formatDate(expense?.expense_date, 'DD MMMM YYYY'), + }, + { + label: 'Tanggal Realisasi', + value: expense?.realization_date + ? formatDate(expense?.realization_date, 'DD MMMM YYYY') + : '-', + }, + { label: 'Nama Pengaju', value: expense?.created_user.name }, + { + label: 'Nominal Biaya', + value: formatCurrency(expense?.grand_total ?? 0), + }, + { + label: 'Nominal Pengajuan', + value: formatCurrency(expense?.total_pengajuan ?? 0), + }, + { + label: 'Nominal Realisasi', + value: expense?.total_realisasi + ? formatCurrency(expense?.total_realisasi ?? 0) + : '-', + }, + { label: 'Status Pencairan', value: realizationStatus }, + { + label: 'Status Biaya', + value: isLatestApprovalRejected + ? 'Ditolak' + : expense?.latest_approval?.step_name, + }, + ]; + + return ( + + + + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + + PT LUMBUNG TELUR INDONESIA + + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + + Laporan{' '} + {expense?.category === 'BOP' + ? 'Biaya Operasional' + : 'Non-Biaya Operasional'}{' '} + {expense?.po_number} + + + {/* General info table */} + + {rows.map((row) => ( + + + + {row.label} + + + + : + + + + {row.value} + + + + ))} + + + {/* Detail expense request */} + + + Rincian Pengajuan Biaya Operasional + + + {expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { + let expenseRequestTotal = 0; + + kandangExpense.pengajuans?.forEach( + (item) => (expenseRequestTotal += item.total_price) + ); + + return ( + + + {kandangExpense.name} + + + + + + + Nonstock + + + + + Kuantitas + + + + + Total Biaya + + + + + Catatan + + + + + {kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => ( + + + + {pengajuan.nonstock.name} + + + + + {formatNumber(pengajuan.qty)} + + + + + {formatCurrency(pengajuan.total_price)} + + + + + {pengajuan.note} + + + + ))} + + + + + Total Biaya Keseluruhan + + + + + {formatCurrency(expenseRequestTotal)} + + + + + + ); + })} + + + {/* Detail expense realization */} + + + Rincian Realisasi Biaya Operasional + + + {expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { + let expenseRealizationTotal = 0; + + kandangExpense.realisasi?.forEach( + (item) => (expenseRealizationTotal += item.total_price) + ); + + return ( + + + {kandangExpense.name} + + + + + + + Nonstock + + + + + Kuantitas + + + + + Total Biaya + + + + + Catatan + + + + + {kandangExpense.realisasi?.map((realisasi, realisasiIdx) => ( + + + + {realisasi.nonstock.name} + + + + + {formatNumber(realisasi.qty)} + + + + + {formatCurrency(realisasi.total_price)} + + + + + {realisasi.note} + + + + ))} + + + + + Total Biaya Keseluruhan + + + + + {formatCurrency(expenseRealizationTotal)} + + + + + + ); + })} + + + + + {expense?.po_number} + + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default ExpensePDF;