refactor(FE): Refactor DailyMarketingReportPDF component for cleaner

structure
This commit is contained in:
rstubryan
2026-02-10 16:06:11 +07:00
parent 4c6ac6e8e1
commit be7b2a0f93
@@ -1,275 +1,226 @@
'use client';
import {
Document,
Image,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer';
import {
DailyMarketingReport,
SalesSummary,
} from '@/types/api/report/marketing';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import {
formatCurrency,
formatDate,
formatNumber,
formatTitleCase,
} from '@/lib/helper';
import {
PdfTable,
PdfColumn,
PdfTbodyCell,
PdfTfootCell,
} from '@/components/helper/pdf/table';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfStatusBadge } from '@/components/helper/pdf/badge/PdfStatusBadge';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
import { PdfPageNumber } from '@/components/helper/pdf/layout/PdfPageNumber';
Font.register({
family: 'Helvetica',
src: 'helvetica',
});
const pdfStyles = StyleSheet.create({
page: {
fontSize: 10,
fontFamily: 'Helvetica',
padding: 20,
backgroundColor: '#FFFFFF',
},
titleSection: {
marginBottom: 10,
},
parameterContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 8,
},
});
interface DailyMarketingReportPDFProps {
data?: DailyMarketingReport;
total?: SalesSummary;
}
const DailyMarketingReportPDFStyle = StyleSheet.create({
page: {
paddingTop: 24,
paddingBottom: 64,
paddingHorizontal: 16, // Reduce padding to fit more columns
orientation: 'landscape',
const getTableColumns = (): PdfColumn[] => [
{ key: 'no', header: 'No', flex: 0.5, align: 'center' },
{ key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' },
{
key: 'do_date',
header: 'Tanggal Delivery Order',
flex: 1.3,
align: 'center',
},
{ key: 'aging', header: 'Aging (Hari)', flex: 0.7, align: 'center' },
{ key: 'warehouse', header: 'Gudang', flex: 1.2, align: 'left' },
{ key: 'customer', header: 'Pelanggan', flex: 1.5, align: 'left' },
{ key: 'sales', header: 'Sales', flex: 1, align: 'left' },
{ key: 'product', header: 'Produk', flex: 1.3, align: 'left' },
{ key: 'do_number', header: 'Nomor DO', flex: 1.2, align: 'left' },
{ key: 'vehicle', header: 'Nomor Polisi', flex: 1, align: 'left' },
{ key: 'marketing_type', header: 'Tipe Marketing', flex: 1, align: 'center' },
{ key: 'qty', header: 'Quantity', flex: 0.7, align: 'right' },
{ key: 'avg_weight', header: 'Rata-Rata (Kg)', flex: 0.8, align: 'right' },
{
key: 'total_weight',
header: 'Total Berat (Kg)',
flex: 0.9,
align: 'right',
},
{ key: 'sales_price', header: 'Harga Jual (Rp)', flex: 0.9, align: 'right' },
{ key: 'hpp_price', header: 'HPP (Rp)', flex: 1.3, align: 'right' },
{ key: 'sales_amount', header: 'Total Jual (Rp)', flex: 1, align: 'right' },
{ key: 'hpp_amount', header: 'Total HPP (Rp)', flex: 1.3, align: 'right' },
];
companyInfoHeader: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 8,
const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => {
return rows.map((row, index) => [
{ key: 'no', value: index + 1 },
{
key: 'so_date',
value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-',
},
companyLogo: {
width: 64,
height: 'auto',
{
key: 'do_date',
value: row.realization_date
? formatDate(row.realization_date, 'DD MMM YY')
: '-',
},
companyInfoHeaderDate: {
paddingTop: 8,
fontSize: 10,
{ key: 'aging', value: row.aging_days ?? '-' },
{ key: 'warehouse', value: row.warehouse?.name ?? '-' },
{ key: 'customer', value: row.customer?.name ?? '-' },
{ key: 'sales', value: row.sales?.name ?? '-' },
{ key: 'product', value: row.product?.name ?? '-' },
{ key: 'do_number', value: row.do_number ?? '-' },
{ key: 'vehicle', value: row.vehicle_number ?? '-' },
{
key: 'marketing_type',
value: row.marketing_type ? (
<View style={{ alignItems: 'center' }}>
<PdfStatusBadge
style={{
backgroundColor:
row.marketing_type.toLowerCase() === 'ayam'
? '#FEF3C7'
: row.marketing_type.toLowerCase() === 'trading'
? '#DBEAFE'
: row.marketing_type.toLowerCase() === 'telur'
? '#D1FAE5'
: '#F5F5F5',
color:
row.marketing_type.toLowerCase() === 'ayam'
? '#92400E'
: row.marketing_type.toLowerCase() === 'trading'
? '#1E40AF'
: row.marketing_type.toLowerCase() === 'telur'
? '#065F46'
: '#333333',
borderColor:
row.marketing_type.toLowerCase() === 'ayam'
? '#FBBF24'
: row.marketing_type.toLowerCase() === 'trading'
? '#60A5FA'
: row.marketing_type.toLowerCase() === 'telur'
? '#34D399'
: '#E5E7EB',
}}
>
{formatTitleCase(row.marketing_type)}
</PdfStatusBadge>
</View>
) : (
'-'
),
},
companyName: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 4,
{ key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' },
{
key: 'avg_weight',
value: formatNumber(row.average_weight_kg ?? 0),
align: 'right',
},
companyAddress: {
fontSize: 8,
maxWidth: 400,
marginBottom: 10,
{
key: 'total_weight',
value: formatNumber(row.total_weight_kg ?? 0),
align: 'right',
},
{
key: 'sales_price',
value: formatCurrency(row.sales_price_per_kg ?? 0),
align: 'right',
},
{
key: 'hpp_price',
value: formatCurrency(row.hpp_price_per_kg ?? 0),
align: 'right',
},
{
key: 'sales_amount',
value: formatCurrency(row.sales_amount ?? 0),
align: 'right',
},
{
key: 'hpp_amount',
value: formatCurrency(row.hpp_amount ?? 0),
align: 'right',
},
]);
};
title: {
marginTop: 16,
fontSize: 14,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => {
if (!summary) return [];
footer: {
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
position: 'absolute',
fontSize: 8,
bottom: 30,
left: 0,
right: 0,
textAlign: 'center',
color: 'grey',
return [
{ key: 'no', value: 'TOTAL' },
{ key: 'so_date', value: '' },
{ key: 'do_date', value: '' },
{ key: 'aging', value: '' },
{ key: 'warehouse', value: '' },
{ key: 'customer', value: '' },
{ key: 'sales', value: '' },
{ key: 'product', value: '' },
{ key: 'do_number', value: '' },
{ key: 'vehicle', value: '' },
{ key: 'marketing_type', value: '' },
{
key: 'qty',
value: formatNumber(summary.total_qty ?? 0),
align: 'right',
},
// Table Styles
table: {
width: '100%',
marginTop: 16,
borderWidth: 1,
borderColor: '#000000',
borderBottomWidth: 0,
fontSize: 7, // Smaller font for report
{
key: 'avg_weight',
value: formatNumber(summary.total_weight_kg ?? 0),
align: 'right',
},
tableRow: {
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: '#000000',
alignItems: 'center',
minHeight: 20,
{
key: 'total_weight',
value: formatNumber(summary.total_weight_kg ?? 0),
align: 'right',
},
tableHeader: {
backgroundColor: '#f0f0f0',
fontWeight: 'bold',
{ key: 'sales_price', value: '' },
{
key: 'hpp_price',
value: formatCurrency(summary.total_hpp_price_per_kg ?? 0),
align: 'right',
},
// Columns definition (Total 100%)
colNo: {
width: '3%',
padding: 2,
textAlign: 'center',
borderRightWidth: 1,
borderRightColor: '#000000',
{
key: 'sales_amount',
value: formatCurrency(summary.total_sales_amount ?? 0),
align: 'right',
},
colSoDate: {
width: '6%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
{
key: 'hpp_amount',
value: formatCurrency(summary.total_hpp_amount ?? 0),
align: 'right',
},
colDoDate: {
width: '6%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colAging: {
width: '3%',
padding: 2,
textAlign: 'center',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colWarehouse: {
width: '7%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colCustomer: {
width: '9%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
}, // Reduced slightly
colSales: {
width: '6%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colProduct: {
width: '8%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
}, // Reduced slightly
colDoNumber: {
width: '7%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colVehicle: {
width: '5%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colMarketingType: {
width: '5%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colQty: {
width: '4%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colAvgWeight: {
width: '4%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colTotalWeight: {
width: '5%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colSalesPrice: {
width: '5%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colHppPrice: {
width: '5%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colSalesAmount: {
width: '6%',
padding: 2,
textAlign: 'right',
borderRightWidth: 1,
borderRightColor: '#000000',
},
colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column
// Text inside columns
cellText: {
fontSize: 6,
},
headerText: {
fontSize: 7,
fontWeight: 'bold',
textAlign: 'center',
},
// Utils
doubleDivider: {
width: '100%',
height: 6,
borderTop: '2px solid black',
borderBottom: '2px solid black',
},
// Summary
summaryContainer: {
marginTop: 12,
flexDirection: 'row',
justifyContent: 'flex-end',
width: '100%',
},
summaryTable: {
width: '30%',
borderWidth: 1,
borderColor: '#000000',
fontSize: 8,
},
summaryRow: {
flexDirection: 'row',
padding: 2,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
summaryLabel: {
width: '50%',
fontWeight: 'bold',
},
summaryValue: {
width: '50%',
textAlign: 'right',
},
});
];
};
const DailyMarketingReportPDF = ({
data,
@@ -280,288 +231,31 @@ const DailyMarketingReportPDF = ({
return (
<Document>
<Page
style={DailyMarketingReportPDFStyle.page}
orientation='landscape'
size='A4'
>
<View>
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}>
<Image
style={DailyMarketingReportPDFStyle.companyLogo}
src='/assets/img/lti-logo.png'
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
{/* Title and Parameters */}
<View style={pdfStyles.titleSection}>
<PdfTypography size='h1' variant='primary'>
Laporan &gt; Penjualan Harian
</PdfTypography>
<View style={pdfStyles.parameterContainer}>
<PdfParamBadge>
Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
</PdfParamBadge>
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View>
</View>
{/* Table */}
<PdfTable
columns={getTableColumns()}
data={getTableData(rows)}
footer={getTableFooter(summary)}
footerLabel='TOTAL'
/>
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}>
{formatDate(Date.now(), 'DD MMMM YYYY')}
</Text>
</View>
<View>
<Text style={DailyMarketingReportPDFStyle.companyName}>
PT LUMBUNG TELUR INDONESIA
</Text>
<Text style={DailyMarketingReportPDFStyle.companyAddress}>
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
Cipedes, Kec. Sukajadi, Kota Bandung 40162
</Text>
<View style={DailyMarketingReportPDFStyle.doubleDivider} />
</View>
</View>
<Text style={DailyMarketingReportPDFStyle.title}>
Laporan Penjualan Harian
</Text>
{/* Data Table */}
<View style={DailyMarketingReportPDFStyle.table}>
{/* Header */}
<View
style={[
DailyMarketingReportPDFStyle.tableRow,
DailyMarketingReportPDFStyle.tableHeader,
]}
>
<View style={DailyMarketingReportPDFStyle.colNo}>
<Text style={DailyMarketingReportPDFStyle.headerText}>No</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSoDate}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Tgl SO
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colDoDate}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Tgl DO
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colAging}>
<Text style={DailyMarketingReportPDFStyle.headerText}>Aging</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Gudang
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colCustomer}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Pelanggan
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSales}>
<Text style={DailyMarketingReportPDFStyle.headerText}>Sales</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colProduct}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Produk
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
<Text style={DailyMarketingReportPDFStyle.headerText}>No DO</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colVehicle}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Plat No
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
<Text style={DailyMarketingReportPDFStyle.headerText}>Tipe</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colQty}>
<Text style={DailyMarketingReportPDFStyle.headerText}>Qty</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Rerata
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
<Text style={DailyMarketingReportPDFStyle.headerText}>Berat</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Hrg Jual
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
HPP/kg
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Total Jual
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
<Text style={DailyMarketingReportPDFStyle.headerText}>
Total HPP
</Text>
</View>
</View>
{/* Rows */}
{rows.map((row, index) => (
<View style={DailyMarketingReportPDFStyle.tableRow} key={index}>
<View style={DailyMarketingReportPDFStyle.colNo}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{index + 1}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSoDate}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatDate(row.so_date, 'DD/MM/YYYY')}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colDoDate}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatDate(row.realization_date, 'DD/MM/YYYY')}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colAging}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.aging_days}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.warehouse?.name}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colCustomer}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.customer?.name}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSales}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.sales.name}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colProduct}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.product?.name}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.do_number}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colVehicle}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.vehicle_number}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{row.marketing_type}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colQty}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatNumber(row.qty)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatNumber(row.average_weight_kg)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatNumber(row.total_weight_kg)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatCurrency(row.sales_price_per_kg)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatCurrency(row.hpp_price_per_kg)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatCurrency(row.sales_amount)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
<Text style={DailyMarketingReportPDFStyle.cellText}>
{formatCurrency(row.hpp_amount)}
</Text>
</View>
</View>
))}
</View>
{/* Summary */}
<View style={DailyMarketingReportPDFStyle.summaryContainer}>
<View style={DailyMarketingReportPDFStyle.summaryTable}>
<View style={DailyMarketingReportPDFStyle.summaryRow}>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total Qty:
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatNumber(summary?.total_qty ?? 0)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.summaryRow}>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total Berat (kg):
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatNumber(summary?.total_weight_kg ?? 0)}
</Text>
</View>
<View style={DailyMarketingReportPDFStyle.summaryRow}>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total Penjualan:
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatCurrency(summary?.total_sales_amount ?? 0)}
</Text>
</View>
<View
style={[
DailyMarketingReportPDFStyle.summaryRow,
{ borderBottomWidth: 0 },
]}
>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total HPP Per KG:
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatCurrency(summary?.total_hpp_price_per_kg ?? 0)}
</Text>
</View>
<View
style={[
DailyMarketingReportPDFStyle.summaryRow,
{ borderBottomWidth: 0 },
]}
>
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
Total HPP:
</Text>
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
{formatCurrency(summary?.total_hpp_amount ?? 0)}
</Text>
</View>
</View>
</View>
<View style={DailyMarketingReportPDFStyle.footer} fixed>
<Text
render={({ pageNumber, totalPages }) =>
`${pageNumber} / ${totalPages}`
}
fixed
/>
</View>
<PdfPageNumber />
</Page>
</Document>
);