refactor(FE): Refactor ExpensePDF component for improved readability

This commit is contained in:
rstubryan
2026-03-07 10:14:10 +07:00
parent a468a5948b
commit c8e76a8558
+238 -561
View File
@@ -1,212 +1,154 @@
'use client'; 'use client';
import { import React from 'react';
Document, import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer';
Image,
Link,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import { Expense } from '@/types/api/expense'; import { Expense } from '@/types/api/expense';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; 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 { interface ExpensePDFProps {
expense?: Expense; expense?: Expense;
} }
const ExpensePDFStyle = StyleSheet.create({ const styles = StyleSheet.create({
page: { page: {
paddingTop: 24, fontSize: 10,
paddingBottom: 64, fontFamily: 'Helvetica',
paddingHorizontal: 32, padding: 20,
backgroundColor: '#FFFFFF',
}, },
titleSection: {
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, marginBottom: 10,
}, },
parameterContainer: {
title: {
marginTop: 16,
fontSize: 16,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', flexWrap: 'wrap',
alignItems: 'center', marginBottom: 8,
paddingHorizontal: 32, },
infoTableSection: {
position: 'absolute', marginBottom: 12,
},
infoTableTitle: {
fontSize: 10, 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', fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
generalInfoTableValueText: {}, tableSection: {
marginBottom: 12,
// expense detail table
expenseDetailContainer: {
width: '100%',
marginTop: 12,
}, },
expenseDetailTitle: { tableTitle: {
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, fontSize: 10,
fontWeight: 'bold',
marginBottom: 6,
color: '#333',
}, },
kandangExpenseTableFooterColTotalExpenseCaption: { emptyText: {
width: '40%', fontSize: 8,
paddingVertical: 6, color: '#666',
paddingHorizontal: 8, fontStyle: 'italic',
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) => { type ExpenseKandang = Expense['kandangs'][number];
type PengajuanItem = NonNullable<ExpenseKandang['pengajuans']>[number];
type RealisasiItem = NonNullable<ExpenseKandang['realisasi']>[number];
const valueText = (v: unknown) => {
if (v === null || v === undefined) return '-';
if (typeof v === 'number') return formatNumber(v);
return String(v);
};
const getPengajuanColumns = (): PdfColumn<PengajuanItem>[] => [
{
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<RealisasiItem>[] => [
{
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 = const isLatestApprovalRejected =
expense?.latest_approval?.action === 'REJECTED'; expense?.latest_approval?.action === 'REJECTED';
const isExpenseRealized = const isExpenseRealized =
expense?.latest_approval?.step_number && expense?.latest_approval?.step_number &&
expense?.latest_approval.step_number >= 5; expense?.latest_approval.step_number >= 5;
const realizationStatus = isExpenseRealized const realizationStatus = isExpenseRealized
? 'Sudah Realisasi' ? 'Sudah Realisasi'
: 'Belum Realisasi'; : 'Belum Realisasi';
const rows = [ return [
{ label: 'Nomor PO', value: expense?.po_number }, { label: 'Nomor PO', value: expense?.po_number || '-' },
{ label: 'Nomor Referensi', value: expense?.reference_number }, { label: 'Nomor Referensi', value: expense?.reference_number || '-' },
{ {
label: 'Kategori', label: 'Kategori',
value: value:
@@ -214,9 +156,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? 'Biaya Operasional' ? 'Biaya Operasional'
: expense?.category === 'NON-BOP' : expense?.category === 'NON-BOP'
? 'Non Biaya Operasional' ? 'Non Biaya Operasional'
: '', : '-',
}, },
{ label: 'Lokasi', value: expense?.location.name }, { label: 'Lokasi', value: expense?.location?.name || '-' },
{ {
label: 'Kandang', label: 'Kandang',
value: value:
@@ -227,7 +169,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
.join(', ') .join(', ')
: '-', : '-',
}, },
{ label: 'Vendor', value: expense?.supplier.name }, { label: 'Vendor', value: expense?.supplier?.name || '-' },
{ {
label: 'Tanggal Transaksi', label: 'Tanggal Transaksi',
value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'),
@@ -238,12 +180,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
? formatDate(expense?.realization_date, 'DD MMMM YYYY') ? 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', label: 'Nominal Biaya',
value: formatCurrency( value: formatCurrency(
expense?.latest_approval.step_number === 5 || expense?.latest_approval?.step_number === 5 ||
expense?.latest_approval.step_number === 6 expense?.latest_approval?.step_number === 6
? (expense?.total_realisasi ?? 0) ? (expense?.total_realisasi ?? 0)
: (expense?.total_pengajuan ?? 0) : (expense?.total_pengajuan ?? 0)
), ),
@@ -263,401 +205,136 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
label: 'Status Biaya', label: 'Status Biaya',
value: isLatestApprovalRejected value: isLatestApprovalRejected
? 'Ditolak' ? 'Ditolak'
: expense?.latest_approval?.step_name, : expense?.latest_approval?.step_name || '-',
},
];
};
interface InfoRow {
label: string;
value: string;
}
const getInfoTableColumns = (): PdfColumn<InfoRow>[] => [
{
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 ( return (
<Document> <Document>
<Page style={ExpensePDFStyle.page}> <Page style={styles.page} size='A4'>
<View> {/* Title Section */}
<View style={ExpensePDFStyle.companyInfoHeader}> <View style={styles.titleSection}>
<Image <PdfTypography size='h1' variant='primary'>
style={ExpensePDFStyle.companyLogo}
src='/assets/img/lti-logo.png'
/>
<Text style={ExpensePDFStyle.companyInfoHeaderDate}>
{formatDate(Date.now(), 'DD MMMM YYYY')}
</Text>
</View>
<View>
<Text style={ExpensePDFStyle.companyName}>
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={ExpensePDFStyle.companyAddress}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={ExpensePDFStyle.doubleDivider} />
</View>
</View>
<Text style={ExpensePDFStyle.title}>
Laporan{' '} Laporan{' '}
{expense?.category === 'BOP' {expense?.category === 'BOP'
? 'Biaya Operasional' ? 'Biaya Operasional'
: 'Non-Biaya Operasional'}{' '} : 'Non-Biaya Operasional'}
{expense?.po_number} </PdfTypography>
</Text> <PdfTypography size='h2'>{expense?.po_number || '-'}</PdfTypography>
<View style={styles.parameterContainer}>
{/* General info table */} <PdfParamBadge>
<View style={ExpensePDFStyle.generalInfoTable}> Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
{rows.map((row) => ( </PdfParamBadge>
<View style={ExpensePDFStyle.generalInfoTableRow} key={row.label}> <PdfParamBadge>
<View style={ExpensePDFStyle.generalInfoTableColLabel}> Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
<Text style={ExpensePDFStyle.generalInfoTableLabelText}> </PdfParamBadge>
{row.label}
</Text>
</View> </View>
<View style={ExpensePDFStyle.generalInfoTableColSeparator}>
<Text>:</Text>
</View>
<View style={ExpensePDFStyle.generalInfoTableColValue}>
<Text style={ExpensePDFStyle.generalInfoTableValueText}>
{row.value}
</Text>
</View>
</View>
))}
</View> </View>
{/* Detail expense request */} {/* Info Table Section */}
<View <View style={styles.infoTableSection}>
minPresenceAhead={80} <Text style={styles.infoTableTitle}>Informasi Biaya</Text>
style={ExpensePDFStyle.expenseDetailContainer} <PdfTable columns={getInfoTableColumns()} data={infoRows} />
> </View>
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Pengajuan Biaya Operasional
</Text>
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => { {/* Rincian Pengajuan Section */}
let expenseRequestTotal = 0; <View style={styles.tableSection}>
<Text style={styles.tableTitle}>1. Rincian Pengajuan Biaya</Text>
kandangExpense.pengajuans?.forEach( {kandangs.length === 0 ? (
(item) => (expenseRequestTotal += item.qty * item.price) <Text style={styles.emptyText}>Tidak ada data pengajuan.</Text>
); ) : (
kandangs.map((kandang, idx) => {
const pengajuans = kandang.pengajuans || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
return ( return (
<View <View key={idx} style={{ marginBottom: 12 }}>
key={kandangExpenseIdx} <PdfTypography size='h3' style={{ paddingLeft: 12 }}>
style={ExpensePDFStyle.kandangExpenseContainer} {idx + 1}) {kandangName}
> </PdfTypography>
<Text style={ExpensePDFStyle.kandangExpenseTitle}> {pengajuans.length > 0 ? (
{kandangExpense.kandang_id && kandangExpense.name <PdfTable
? `Biaya ${kandangExpense.name}` columns={getPengajuanColumns()}
: `Biaya ${expense?.location.name || 'Umum'}`} data={pengajuans}
</Text> showFooter={true}
footerLabel='Total'
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.pengajuans?.map((pengajuan, pengajuanIdx) => (
<View
key={pengajuanIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(pengajuan.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(pengajuan.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{pengajuan.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRequestTotal)}
</Text>
</View>
</View>
</View>
</View>
);
})}
</View>
{/* Detail expense realization */}
<View
minPresenceAhead={80}
style={ExpensePDFStyle.expenseDetailContainer}
>
<Text style={ExpensePDFStyle.expenseDetailTitle}>
Rincian Realisasi Biaya Operasional
</Text>
{expense?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
let expenseRealizationTotal = 0;
kandangExpense.realisasi?.forEach(
(item) => (expenseRealizationTotal += item.qty * item.price)
);
return (
<View
key={kandangExpenseIdx}
style={ExpensePDFStyle.kandangExpenseContainer}
>
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${expense?.location.name || 'Umum'}`}
</Text>
<View style={ExpensePDFStyle.kandangExpenseTable}>
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Nonstock
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Kuantitas
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Harga Satuan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Catatan
</Text>
</View>
</View>
{kandangExpense.realisasi?.map((realisasi, realisasiIdx) => (
<View
key={realisasiIdx}
style={ExpensePDFStyle.kandangExpenseTableRow}
>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
ExpensePDFStyle.kandangExpenseTableColNonstock,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.nonstock.name}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatNumber(realisasi.qty)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{formatCurrency(realisasi.price)}
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableColLabel,
ExpensePDFStyle.kandangExpenseTableColNote,
]}
>
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
{realisasi.notes}
</Text>
</View>
</View>
))}
<View style={[ExpensePDFStyle.kandangExpenseTableRow]}>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseCaption,
ExpensePDFStyle.kandangExpenseTableColLabelBorderRight,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
Total Biaya Keseluruhan
</Text>
</View>
<View
style={[
ExpensePDFStyle.kandangExpenseTableFooterColTotalExpenseValue,
]}
>
<Text
style={ExpensePDFStyle.kandangExpenseHeaderLabelText}
>
{formatCurrency(expenseRealizationTotal)}
</Text>
</View>
</View>
</View>
</View>
);
})}
</View>
<View style={ExpensePDFStyle.footer} fixed>
<Link
src={`${process.env.NEXT_PUBLIC_LTI_URL}expense/detail?expenseId=${expense?.id}`}
>
{expense?.po_number}
</Link>
<Text
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/> />
) : (
<Text style={styles.emptyText}>
Tidak ada item pengajuan untuk kandang ini.
</Text>
)}
</View> </View>
);
})
)}
</View>
{/* Rincian Realisasi Section */}
<View style={styles.tableSection}>
<Text style={styles.tableTitle}>2. Rincian Realisasi Biaya</Text>
{kandangs.length === 0 ? (
<Text style={styles.emptyText}>Tidak ada data realisasi.</Text>
) : (
kandangs.map((kandang, idx) => {
const realisasi = kandang.realisasi || [];
const kandangName =
kandang.kandang_id && kandang.name
? kandang.name
: expense?.location?.name || 'Umum';
return (
<View key={idx} style={{ marginBottom: 12 }}>
<PdfTypography size='h3' style={{ paddingLeft: 12 }}>
{idx + 1}) {kandangName}
</PdfTypography>
{realisasi.length > 0 ? (
<PdfTable
columns={getRealisasiColumns()}
data={realisasi}
showFooter={true}
footerLabel='Total'
/>
) : (
<Text style={styles.emptyText}>
Tidak ada item realisasi untuk kandang ini.
</Text>
)}
</View>
);
})
)}
</View>
<PdfPageNumber />
</Page> </Page>
</Document> </Document>
); );