refactor(FE): Refactor DebtSupplierExportPDF to use reusable PDF

components
This commit is contained in:
rstubryan
2026-02-09 21:50:35 +07:00
parent e4e6e563c9
commit bcc2070ed2
@@ -2,7 +2,6 @@
import {
Page,
Text,
View,
Document,
StyleSheet,
@@ -12,6 +11,15 @@ import {
import { formatDate, formatCurrency, formatNumber } from '@/lib/helper';
import { DebtSupplier } from '@/types/api/report/debt-supplier';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
import {
PdfTable,
PdfColumn,
PdfTbodyCell,
PdfTfootCell,
} from '@/components/helper/pdf/table';
Font.register({
family: 'Helvetica',
@@ -69,133 +77,6 @@ const pdfStyles = StyleSheet.create({
titleSection: {
marginBottom: 10,
},
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
color: '#1f74bf',
},
supplierTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
supplierInfo: {
fontSize: 9,
marginBottom: 5,
color: '#333333',
},
table: {
borderWidth: 1,
borderColor: '#000000',
marginBottom: 15,
},
tableRow: {
flexDirection: 'row',
},
tableHeader: {
backgroundColor: '#F5F5F5',
},
tableCell: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'left',
},
tableCellNo: {
flex: 0.5,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'center',
},
tableCellLast: {
flex: 1,
padding: 4,
fontSize: 7,
},
tableCellHeader: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
textAlign: 'center',
},
tableCellHeaderRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
fontWeight: 'bold',
backgroundColor: '#F5F5F5',
textAlign: 'right',
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
paddingVertical: 12,
},
tableCellRight: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'right',
},
tableCellCenter: {
flex: 1,
borderRightWidth: 1,
borderRightColor: '#000000',
borderRightStyle: 'solid',
padding: 4,
fontSize: 7,
textAlign: 'center',
},
tableBorderBottom: {
borderBottomWidth: 1,
borderBottomColor: '#000000',
borderBottomStyle: 'solid',
},
summaryRow: {
backgroundColor: '#F0F0F0',
fontWeight: 'bold',
},
badge: {
paddingVertical: 2,
paddingHorizontal: 4,
borderRadius: 12,
fontSize: 5,
fontWeight: 'bold',
borderWidth: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
@@ -203,6 +84,153 @@ const pdfStyles = StyleSheet.create({
},
});
const getTableColumns = (): PdfColumn[] => [
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
{ key: 'pr_number', header: 'No. PR', flex: 1, align: 'left' },
{ key: 'po_number', header: 'No. PO', flex: 1, align: 'left' },
{
key: 'received_date',
header: 'Tgl Terima/Bayar',
flex: 0.7,
align: 'center',
},
{ key: 'po_date', header: 'Tgl PO', flex: 0.7, align: 'center' },
{ key: 'aging', header: 'Aging', flex: 0.6, align: 'center' },
{ key: 'area', header: 'Area', flex: 1, align: 'left' },
{ key: 'warehouse', header: 'Gudang', flex: 1, align: 'left' },
{ key: 'due_date', header: 'Jatuh Tempo', flex: 1, align: 'center' },
{ key: 'due_status', header: 'Status Jatuh Tempo', flex: 2, align: 'left' },
{
key: 'total_price',
header: 'Nominal Pembelian (Rp)',
flex: 1.5,
align: 'right',
},
{
key: 'payment_price',
header: 'Pembayaran (Rp)',
flex: 1.5,
align: 'right',
},
{
key: 'balance',
header: 'Sisa Saldo Hutang (Rp)',
flex: 1.5,
align: 'right',
},
{ key: 'status', header: 'Status', flex: 1.2, align: 'center' },
{ key: 'travel_number', header: 'No. Perjalanan', flex: 1, align: 'left' },
];
const getTableData = (rows: DebtSupplier['rows']): PdfTbodyCell[][] => {
return rows.map((item, index) => [
{ key: 'no', value: index + 1 },
{ key: 'pr_number', value: item.pr_number || '-' },
{ key: 'po_number', value: item.po_number || '-' },
{
key: 'received_date',
value: item.received_date
? formatDate(item.received_date, 'DD MMM YY')
: '-',
},
{
key: 'po_date',
value: item.po_date ? formatDate(item.po_date, 'DD MMM YY') : '-',
},
{
key: 'aging',
value: item.aging != null ? `${formatNumber(item.aging)}` : '-',
},
{ key: 'area', value: item.area?.name || '-' },
{ key: 'warehouse', value: item.warehouse?.name || '-' },
{
key: 'due_date',
value: item.due_date ? formatDate(item.due_date, 'DD MMM YY') : '-',
},
{
key: 'due_status',
value:
item.due_status && item.due_status !== '-' ? (
<PdfStatusBadge
backgroundColor={getPDFBadgeStyle(item.due_status, 'due').bg}
textColor={getPDFBadgeStyle(item.due_status, 'due').text}
borderColor={getPDFBadgeStyle(item.due_status, 'due').border}
>
{item.due_status}
</PdfStatusBadge>
) : (
'-'
),
},
{
key: 'total_price',
value: formatCurrency(item.total_price),
align: 'right',
color: item.total_price < 0 ? 'red' : undefined,
},
{
key: 'payment_price',
value: formatCurrency(item.payment_price),
align: 'right',
color: item.payment_price < 0 ? 'red' : undefined,
},
{
key: 'balance',
value: formatCurrency(item.balance),
align: 'right',
color: item.balance < 0 ? 'red' : undefined,
},
{
key: 'status',
value:
item.status && item.status !== '-' ? (
<View style={{ alignItems: 'center' }}>
<PdfStatusBadge
backgroundColor={getPDFBadgeStyle(item.status, 'payment').bg}
textColor={getPDFBadgeStyle(item.status, 'payment').text}
borderColor={getPDFBadgeStyle(item.status, 'payment').border}
>
{item.status}
</PdfStatusBadge>
</View>
) : (
'-'
),
},
{ key: 'travel_number', value: item.travel_number || '-' },
]);
};
const getTableFooter = (total: DebtSupplier['total']): PdfTfootCell[] => [
{ key: 'no', value: 'Total' },
{ key: 'pr_number', value: '' },
{ key: 'po_number', value: '' },
{ key: 'received_date', value: '' },
{ key: 'po_date', value: '' },
{ key: 'aging', value: formatNumber(total?.aging || 0) + ' Hari' },
{ key: 'area', value: '' },
{ key: 'warehouse', value: '' },
{ key: 'due_date', value: '' },
{ key: 'due_status', value: '' },
{
key: 'total_price',
value: formatCurrency(total?.total_price || 0),
align: 'right',
},
{
key: 'payment_price',
value: formatCurrency(total?.payment_price || 0),
align: 'right',
},
{
key: 'balance',
value: formatCurrency(total?.debt_price || 0),
align: 'right',
},
{ key: 'status', value: '' },
{ key: 'travel_number', value: '' },
];
interface DebtSupplierExportPDFParams {
data: DebtSupplier[];
params?: {
@@ -219,418 +247,74 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
{params.data.map((supplierReport, supplierIndex) => (
<Page
key={supplierIndex}
size='A4'
size='A3'
orientation='landscape'
style={pdfStyles.page}
>
{/* Title and Supplier Info */}
<View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}>
<PdfTypography size='h1' variant='primary'>
Laporan &gt; Rekapitulasi Hutang ke Supplier
</Text>
</PdfTypography>
<View style={pdfStyles.parameterContainer}>
<View style={pdfStyles.parameterBadge}>
<Text>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</Text>
</View>
<PdfParamBadge>
Periode:{' '}
{params.params?.start_date
? formatDate(params.params.start_date, 'DD MMM YYYY')
: '-'}{' '}
s.d{' '}
{params.params?.end_date
? formatDate(params.params.end_date, 'DD MMM YYYY')
: '-'}
</PdfParamBadge>
{params.params?.filter_by && (
<View style={pdfStyles.parameterBadge}>
<Text>
Filter Tanggal:{' '}
{params.params.filter_by === 'po_date'
? 'Tanggal PO'
: params.params.filter_by === 'received_date'
? 'Tanggal Terima'
: params.params.filter_by === 'due_date'
? 'Tanggal Jatuh Tempo'
: params.params.filter_by}
</Text>
</View>
<PdfParamBadge>
Filter Tanggal:{' '}
{params.params.filter_by === 'po_date'
? 'Tanggal PO'
: params.params.filter_by === 'received_date'
? 'Tanggal Terima'
: params.params.filter_by === 'due_date'
? 'Tanggal Jatuh Tempo'
: params.params.filter_by}
</PdfParamBadge>
)}
<View style={pdfStyles.parameterBadge}>
<Text>
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
</Text>
</View>
<View style={pdfStyles.parameterBadge}>
<Text>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</Text>
</View>
<PdfParamBadge>
Supplier: {params.params?.supplier_name || 'Semua Supplier'}
</PdfParamBadge>
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View>
<Text style={pdfStyles.supplierTitle}>
<PdfTypography size='h2' variant='primary'>
{supplierReport.supplier.name}
</Text>
<Text style={pdfStyles.supplierInfo}>
</PdfTypography>
<PdfTypography size='label'>
{supplierReport.supplier.category}
</Text>
</PdfTypography>
</View>
{/* Table */}
<View style={pdfStyles.table}>
{/* Table Header */}
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
<Text>No</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PR</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>No. PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
<Text>Tgl Terima/Bayar</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.7 }]}>
<Text>Tgl PO</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 0.6 }]}>
<Text>Aging</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Area</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Gudang</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Jatuh Tempo</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 2 }]}>
<Text>Status Jatuh Tempo</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Nominal Pembelian (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Pembayaran (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Sisa Saldo Hutang (Rp)</Text>
</View>
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
<Text>Status</Text>
</View>
<View
style={[
pdfStyles.tableCellHeader,
{ flex: 1, borderRight: 'none' },
]}
>
<Text>No. Perjalanan</Text>
</View>
</View>
{/* Initial Balance Row */}
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text></Text> {/* NO */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* No. PR */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* No. PO */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text></Text> {/* Tgl Terima/Bayar */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text></Text> {/* Tgl PO */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text></Text> {/* Aging */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* Area */}
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text> {/* Gudang */}
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text> {/* Jatuh Tempo */}
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text> {/* Status Jatuh Tempo */}
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> {/* Nominal Pembelian (Rp) */}
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text> {/* Pembayaran (Rp) */}
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
<PdfTable
columns={getTableColumns()}
data={getTableData(supplierReport.rows)}
footer={
supplierReport.total
? getTableFooter(supplierReport.total)
: undefined
}
firstRow={
typeof supplierReport.initial_balance === 'number' &&
supplierReport.initial_balance !== 0
? {
valueKey: 'balance',
value: supplierReport.initial_balance,
align: 'right',
color: supplierReport.initial_balance < 0 ? 'red' : 'black',
},
]}
>
<Text>
{' '}
{/* Sisa Saldo Hutang (Rp) */}
{formatCurrency(supplierReport.initial_balance || 0)}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text> {/* Status */}
</View>
<View
style={[
pdfStyles.tableCell, // No. Perjalanan
{ flex: 1, borderRight: 'none' },
]}
>
<Text></Text>
</View>
</View>
{/* Table Body */}
{supplierReport.rows.map((item, index) => (
<View
key={index}
style={[
pdfStyles.tableRow,
index < supplierReport.rows.length - 1
? pdfStyles.tableBorderBottom
: {},
]}
>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>{index + 1}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.pr_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.po_number || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text>
{item.received_date
? item.received_date != '-'
? formatDate(item.received_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.7 }]}>
<Text>
{item.po_date
? item.po_date != '-'
? formatDate(item.po_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text>{formatNumber(item.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.area?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.warehouse?.name || '-'}</Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text>
{item.due_date
? item.due_date != '-'
? formatDate(item.due_date, 'DD MMM YY')
: '-'
: '-'}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
{item.due_status && item.due_status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.due_status,
'due'
).bg,
borderColor: getPDFBadgeStyle(item.due_status, 'due')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.due_status, 'due').text,
}}
>
{item.due_status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color: item.total_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(item.total_price)}</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color: item.payment_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(item.payment_price)}</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{ flex: 1.5, color: item.balance < 0 ? 'red' : 'black' },
]}
>
<Text>{formatCurrency(item.balance)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
{item.status && item.status !== '-' ? (
<View
style={[
pdfStyles.badge,
{
backgroundColor: getPDFBadgeStyle(
item.status,
'payment'
).bg,
borderColor: getPDFBadgeStyle(item.status, 'payment')
.border,
},
]}
>
<Text
style={{
color: getPDFBadgeStyle(item.status, 'payment').text,
}}
>
{item.status}
</Text>
</View>
) : (
<Text>-</Text>
)}
</View>
<View
style={[
pdfStyles.tableCell, // No. Perjalanan
{ flex: 1, borderRight: 'none' },
]}
>
<Text>{item.travel_number || '-'}</Text>
</View>
</View>
))}
{/* Summary Row */}
{supplierReport.total && (
<View style={[pdfStyles.tableRow, pdfStyles.summaryRow]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text>Total</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 0.7 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text>{formatNumber(supplierReport.total.aging)} Hari</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 2 }]}>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.total_price < 0 ? 'red' : 'black',
},
]}
>
<Text>
{formatCurrency(supplierReport.total.total_price)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.payment_price < 0
? 'red'
: 'black',
},
]}
>
<Text>
{formatCurrency(supplierReport.total.payment_price)}
</Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color:
supplierReport.total.debt_price < 0 ? 'red' : 'black',
},
]}
>
<Text>{formatCurrency(supplierReport.total.debt_price)}</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellLast, { flex: 1 }]}>
<Text></Text>
</View>
</View>
)}
</View>
}
: undefined
}
/>
</Page>
))}
</Document>