diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx index f76b6f11..34dbef41 100644 --- a/src/components/pages/expense/pdf/ExpensePDF.tsx +++ b/src/components/pages/expense/pdf/ExpensePDF.tsx @@ -1,212 +1,154 @@ 'use client'; -import { - Document, - Image, - Link, - Page, - StyleSheet, - Text, - View, -} from '@react-pdf/renderer'; +import React from 'react'; +import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer'; import { Expense } from '@/types/api/expense'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography'; +import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge'; +import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber'; +import { PdfTable, PdfColumn } from '@/components/helper/pdf/table'; interface ExpensePDFProps { expense?: Expense; } -const ExpensePDFStyle = StyleSheet.create({ +const styles = StyleSheet.create({ page: { - paddingTop: 24, - paddingBottom: 64, - paddingHorizontal: 32, + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', }, - - 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, + titleSection: { marginBottom: 10, }, - - title: { - marginTop: 16, - fontSize: 16, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, - - footer: { - width: '100%', - display: 'flex', + parameterContainer: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 32, - - position: 'absolute', + flexWrap: 'wrap', + marginBottom: 8, + }, + infoTableSection: { + marginBottom: 12, + }, + infoTableTitle: { 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', + marginBottom: 6, + color: '#333', }, - generalInfoTableValueText: {}, - - // expense detail table - expenseDetailContainer: { - width: '100%', - marginTop: 12, + tableSection: { + marginBottom: 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: { + tableTitle: { fontSize: 10, + fontWeight: 'bold', + marginBottom: 6, + color: '#333', }, - 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', + emptyText: { + fontSize: 8, + color: '#666', + fontStyle: 'italic', }, }); -const ExpensePDF = ({ expense }: ExpensePDFProps) => { +type ExpenseKandang = Expense['kandangs'][number]; +type PengajuanItem = NonNullable[number]; +type RealisasiItem = NonNullable[number]; + +const valueText = (v: unknown) => { + if (v === null || v === undefined) return '-'; + if (typeof v === 'number') return formatNumber(v); + return String(v); +}; + +const getPengajuanColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ index }) => index + 1, + }, + { + key: 'nonstock', + header: 'Nonstock', + flex: 1.5, + cell: ({ row }) => row.nonstock.name, + }, + { + key: 'qty', + header: 'Kuantitas', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.qty), + }, + { + key: 'price', + header: 'Harga Satuan', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.price), + }, + { + key: 'notes', + header: 'Catatan', + flex: 1.5, + cell: ({ row }) => row.notes || '-', + }, +]; + +const getRealisasiColumns = (): PdfColumn[] => [ + { + key: 'no', + header: 'No', + flex: 0.5, + align: 'center', + cell: ({ index }) => index + 1, + }, + { + key: 'nonstock', + header: 'Nonstock', + flex: 1.5, + cell: ({ row }) => row.nonstock.name, + }, + { + key: 'qty', + header: 'Kuantitas', + flex: 1, + align: 'right', + cell: ({ row }) => valueText(row.qty), + }, + { + key: 'price', + header: 'Harga Satuan', + flex: 1.2, + align: 'right', + cell: ({ row }) => formatCurrency(row.price), + }, + { + key: 'notes', + header: 'Catatan', + flex: 1.5, + cell: ({ row }) => row.notes || '-', + }, +]; + +const getInfoTableRows = (expense?: Expense) => { const isLatestApprovalRejected = expense?.latest_approval?.action === 'REJECTED'; const isExpenseRealized = expense?.latest_approval?.step_number && expense?.latest_approval.step_number >= 5; - const realizationStatus = isExpenseRealized ? 'Sudah Realisasi' : 'Belum Realisasi'; - const rows = [ - { label: 'Nomor PO', value: expense?.po_number }, - { label: 'Nomor Referensi', value: expense?.reference_number }, + return [ + { label: 'Nomor PO', value: expense?.po_number || '-' }, + { label: 'Nomor Referensi', value: expense?.reference_number || '-' }, { label: 'Kategori', value: @@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { ? 'Biaya Operasional' : expense?.category === 'NON-BOP' ? 'Non Biaya Operasional' - : '', + : '-', }, - { label: 'Lokasi', value: expense?.location.name }, + { label: 'Lokasi', value: expense?.location?.name || '-' }, { label: 'Kandang', value: @@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { .join(', ') : '-', }, - { label: 'Vendor', value: expense?.supplier.name }, + { label: 'Vendor', value: expense?.supplier?.name || '-' }, { label: 'Tanggal Transaksi', value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), @@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { ? formatDate(expense?.realization_date, 'DD MMMM YYYY') : '-', }, - { label: 'Nama Pengaju', value: expense?.created_user.name }, + { label: 'Nama Pengaju', value: expense?.created_user?.name || '-' }, { label: 'Nominal Biaya', value: formatCurrency( - expense?.latest_approval.step_number === 5 || - expense?.latest_approval.step_number === 6 + expense?.latest_approval?.step_number === 5 || + expense?.latest_approval?.step_number === 6 ? (expense?.total_realisasi ?? 0) : (expense?.total_pengajuan ?? 0) ), @@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { label: 'Status Biaya', value: isLatestApprovalRejected ? 'Ditolak' - : expense?.latest_approval?.step_name, + : expense?.latest_approval?.step_name || '-', }, ]; +}; + +interface InfoRow { + label: string; + value: string; +} + +const getInfoTableColumns = (): PdfColumn[] => [ + { + key: 'label', + header: 'Field', + flex: 1, + cell: ({ row }) => row.label, + }, + { + key: 'value', + header: 'Value', + flex: 2, + cell: ({ row }) => row.value, + }, +]; + +const ExpensePDF = ({ expense }: ExpensePDFProps) => { + const kandangs = expense?.kandangs || []; + const infoRows = getInfoTableRows(expense); 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 - - - + + {/* Title Section */} + + + Laporan{' '} + {expense?.category === 'BOP' + ? 'Biaya Operasional' + : 'Non-Biaya Operasional'} + + {expense?.po_number || '-'} + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - Laporan{' '} - {expense?.category === 'BOP' - ? 'Biaya Operasional' - : 'Non-Biaya Operasional'}{' '} - {expense?.po_number} - - - {/* General info table */} - - {rows.map((row) => ( - - - - {row.label} - - - - : - - - - {row.value} - - - - ))} + {/* Info Table Section */} + + Informasi Biaya + - {/* Detail expense request */} - - - Rincian Pengajuan Biaya Operasional - + {/* Rincian Pengajuan Section */} + + 1. Rincian Pengajuan Biaya + {kandangs.length === 0 ? ( + Tidak ada data pengajuan. + ) : ( + kandangs.map((kandang, idx) => { + const pengajuans = kandang.pengajuans || []; + const kandangName = + kandang.kandang_id && kandang.name + ? kandang.name + : expense?.location?.name || 'Umum'; - {expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { - let expenseRequestTotal = 0; - - kandangExpense.pengajuans?.forEach( - (item) => (expenseRequestTotal += item.qty * item.price) - ); - - return ( - - - {kandangExpense.kandang_id && kandangExpense.name - ? `Biaya ${kandangExpense.name}` - : `Biaya ${expense?.location.name || 'Umum'}`} - - - - - - - Nonstock - - - - - Kuantitas - - - - - Harga Satuan - - - - - Catatan - - - - - {kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => ( - - - - {pengajuan.nonstock.name} - - - - - {formatNumber(pengajuan.qty)} - - - - - {formatCurrency(pengajuan.price)} - - - - - {pengajuan.notes} - - - - ))} - - - - - Total Biaya Keseluruhan - - - - - {formatCurrency(expenseRequestTotal)} - - - + return ( + + + {idx + 1}) {kandangName} + + {pengajuans.length > 0 ? ( + + ) : ( + + Tidak ada item pengajuan untuk kandang ini. + + )} - - ); - })} + ); + }) + )} - {/* Detail expense realization */} - - - Rincian Realisasi Biaya Operasional - + {/* Rincian Realisasi Section */} + + 2. Rincian Realisasi Biaya + {kandangs.length === 0 ? ( + Tidak ada data realisasi. + ) : ( + kandangs.map((kandang, idx) => { + const realisasi = kandang.realisasi || []; + const kandangName = + kandang.kandang_id && kandang.name + ? kandang.name + : expense?.location?.name || 'Umum'; - {expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { - let expenseRealizationTotal = 0; - - kandangExpense.realisasi?.forEach( - (item) => (expenseRealizationTotal += item.qty * item.price) - ); - - return ( - - - {kandangExpense.kandang_id && kandangExpense.name - ? `Biaya ${kandangExpense.name}` - : `Biaya ${expense?.location.name || 'Umum'}`} - - - - - - - Nonstock - - - - - Kuantitas - - - - - Harga Satuan - - - - - Catatan - - - - - {kandangExpense.realisasi?.map((realisasi, realisasiIdx) => ( - - - - {realisasi.nonstock.name} - - - - - {formatNumber(realisasi.qty)} - - - - - {formatCurrency(realisasi.price)} - - - - - {realisasi.notes} - - - - ))} - - - - - Total Biaya Keseluruhan - - - - - {formatCurrency(expenseRealizationTotal)} - - - + return ( + + + {idx + 1}) {kandangName} + + {realisasi.length > 0 ? ( + + ) : ( + + Tidak ada item realisasi untuk kandang ini. + + )} - - ); - })} + ); + }) + )} - - - {expense?.po_number} - - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - + );