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'; 'use client';
import { import { Page, View, Document, StyleSheet, Font } from '@react-pdf/renderer';
Document,
Image,
Page,
StyleSheet,
Text,
View,
} from '@react-pdf/renderer';
import { import {
DailyMarketingReport, DailyMarketingReport,
SalesSummary, SalesSummary,
} from '@/types/api/report/marketing'; } 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 { interface DailyMarketingReportPDFProps {
data?: DailyMarketingReport; data?: DailyMarketingReport;
total?: SalesSummary; total?: SalesSummary;
} }
const DailyMarketingReportPDFStyle = StyleSheet.create({ const getTableColumns = (): PdfColumn[] => [
page: { { key: 'no', header: 'No', flex: 0.5, align: 'center' },
paddingTop: 24, { key: 'so_date', header: 'Tanggal Sales Order', flex: 1.3, align: 'center' },
paddingBottom: 64, {
paddingHorizontal: 16, // Reduce padding to fit more columns key: 'do_date',
orientation: 'landscape', 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: { const getTableData = (rows: DailyMarketingReport): PdfTbodyCell[][] => {
width: '100%', return rows.map((row, index) => [
display: 'flex', { key: 'no', value: index + 1 },
flexDirection: 'row', {
justifyContent: 'space-between', key: 'so_date',
alignItems: 'flex-start', value: row.so_date ? formatDate(row.so_date, 'DD MMM YY') : '-',
marginBottom: 8, },
}, {
companyLogo: { key: 'do_date',
width: 64, value: row.realization_date
height: 'auto', ? formatDate(row.realization_date, 'DD MMM YY')
}, : '-',
companyInfoHeaderDate: { },
paddingTop: 8, { key: 'aging', value: row.aging_days ?? '-' },
fontSize: 10, { key: 'warehouse', value: row.warehouse?.name ?? '-' },
}, { key: 'customer', value: row.customer?.name ?? '-' },
companyName: { { key: 'sales', value: row.sales?.name ?? '-' },
fontSize: 12, { key: 'product', value: row.product?.name ?? '-' },
fontWeight: 'bold', { key: 'do_number', value: row.do_number ?? '-' },
marginBottom: 4, { key: 'vehicle', value: row.vehicle_number ?? '-' },
}, {
companyAddress: { key: 'marketing_type',
fontSize: 8, value: row.marketing_type ? (
maxWidth: 400, <View style={{ alignItems: 'center' }}>
marginBottom: 10, <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>
) : (
'-'
),
},
{ key: 'qty', value: formatNumber(row.qty ?? 0), align: 'right' },
{
key: 'avg_weight',
value: formatNumber(row.average_weight_kg ?? 0),
align: 'right',
},
{
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: { const getTableFooter = (summary?: SalesSummary): PdfTfootCell[] => {
marginTop: 16, if (!summary) return [];
fontSize: 14,
lineHeight: '150%',
textAlign: 'center',
fontFamily: 'Times-Roman',
fontWeight: 'bold',
},
footer: { return [
width: '100%', { key: 'no', value: 'TOTAL' },
display: 'flex', { key: 'so_date', value: '' },
flexDirection: 'row', { key: 'do_date', value: '' },
justifyContent: 'space-between', { key: 'aging', value: '' },
alignItems: 'center', { key: 'warehouse', value: '' },
paddingHorizontal: 16, { key: 'customer', value: '' },
{ key: 'sales', value: '' },
position: 'absolute', { key: 'product', value: '' },
fontSize: 8, { key: 'do_number', value: '' },
bottom: 30, { key: 'vehicle', value: '' },
left: 0, { key: 'marketing_type', value: '' },
right: 0, {
textAlign: 'center', key: 'qty',
color: 'grey', value: formatNumber(summary.total_qty ?? 0),
}, align: 'right',
},
// Table Styles {
table: { key: 'avg_weight',
width: '100%', value: formatNumber(summary.total_weight_kg ?? 0),
marginTop: 16, align: 'right',
borderWidth: 1, },
borderColor: '#000000', {
borderBottomWidth: 0, key: 'total_weight',
fontSize: 7, // Smaller font for report value: formatNumber(summary.total_weight_kg ?? 0),
}, align: 'right',
tableRow: { },
flexDirection: 'row', { key: 'sales_price', value: '' },
borderBottomWidth: 1, {
borderBottomColor: '#000000', key: 'hpp_price',
alignItems: 'center', value: formatCurrency(summary.total_hpp_price_per_kg ?? 0),
minHeight: 20, align: 'right',
}, },
tableHeader: { {
backgroundColor: '#f0f0f0', key: 'sales_amount',
fontWeight: 'bold', value: formatCurrency(summary.total_sales_amount ?? 0),
}, align: 'right',
},
// Columns definition (Total 100%) {
colNo: { key: 'hpp_amount',
width: '3%', value: formatCurrency(summary.total_hpp_amount ?? 0),
padding: 2, align: 'right',
textAlign: 'center', },
borderRightWidth: 1, ];
borderRightColor: '#000000', };
},
colSoDate: {
width: '6%',
padding: 2,
textAlign: 'left',
borderRightWidth: 1,
borderRightColor: '#000000',
},
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 = ({ const DailyMarketingReportPDF = ({
data, data,
@@ -280,288 +231,31 @@ const DailyMarketingReportPDF = ({
return ( return (
<Document> <Document>
<Page <Page size='A3' orientation='landscape' style={pdfStyles.page}>
style={DailyMarketingReportPDFStyle.page} {/* Title and Parameters */}
orientation='landscape' <View style={pdfStyles.titleSection}>
size='A4' <PdfTypography size='h1' variant='primary'>
> Laporan &gt; Penjualan Harian
<View> </PdfTypography>
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}> <View style={pdfStyles.parameterContainer}>
<Image <PdfParamBadge>
style={DailyMarketingReportPDFStyle.companyLogo} Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')}
src='/assets/img/lti-logo.png' </PdfParamBadge>
/> <PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}> </PdfParamBadge>
{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>
</View> </View>
<Text style={DailyMarketingReportPDFStyle.title}> {/* Table */}
Laporan Penjualan Harian <PdfTable
</Text> columns={getTableColumns()}
data={getTableData(rows)}
footer={getTableFooter(summary)}
footerLabel='TOTAL'
/>
{/* Data Table */} <PdfPageNumber />
<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>
</Page> </Page>
</Document> </Document>
); );