refactor(FE): Refactor HppPerKandang PDF generation logic and UI

renderer
This commit is contained in:
rstubryan
2026-02-10 12:01:51 +07:00
parent d0dea834c1
commit 4775c1e115
2 changed files with 174 additions and 145 deletions
@@ -2,7 +2,6 @@
import { import {
Page, Page,
Text,
View, View,
Document, Document,
StyleSheet, StyleSheet,
@@ -21,6 +20,8 @@ import {
PdfTbodyCell, PdfTbodyCell,
PdfTfootCell, PdfTfootCell,
} from '@/components/helper/pdf/table'; } from '@/components/helper/pdf/table';
import { PdfParamBadge } from '@/components/helper/pdf/badge/PdfParamBadge';
import { PdfTypography } from '@/components/helper/pdf/typography/PdfTypography';
Font.register({ Font.register({
family: 'Helvetica', family: 'Helvetica',
@@ -37,27 +38,6 @@ const pdfStyles = StyleSheet.create({
titleSection: { titleSection: {
marginBottom: 10, marginBottom: 10,
}, },
mainTitle: {
fontSize: 14,
fontWeight: 'bold',
marginBottom: 5,
color: '#1f74bf',
},
supplierTitle: {
fontSize: 12,
fontWeight: 'bold',
marginBottom: 8,
color: '#1f74bf',
},
parameterBadge: {
backgroundColor: '#F5F5F5',
color: '#333333',
padding: 4,
borderRadius: 4,
fontSize: 8,
marginRight: 8,
marginBottom: 4,
},
parameterContainer: { parameterContainer: {
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
@@ -70,7 +50,7 @@ const pdfStyles = StyleSheet.create({
interface HppPerKandangExportParams { interface HppPerKandangExportParams {
data: HppPerKandangReport; data: HppPerKandangReport;
params: { params?: {
area_name?: string; area_name?: string;
location_name?: string; location_name?: string;
kandang_name?: string; kandang_name?: string;
@@ -82,44 +62,11 @@ interface HppPerKandangExportParams {
}; };
} }
const getParameterText = (params: HppPerKandangExportParams['params']) => { const formatSuppliers = (
const paramsText = []; suppliers: { alias?: string; name: string }[] | null | undefined
): string => {
if (params.area_name && params.area_name !== 'Semua Area') { if (!suppliers || suppliers.length === 0) return '-';
paramsText.push(`Area: ${params.area_name}`); return suppliers.map((s) => s.alias || s.name).join(' | ');
}
if (params.location_name && params.location_name !== 'Semua Lokasi') {
paramsText.push(`Lokasi: ${params.location_name}`);
}
if (params.kandang_name && params.kandang_name !== 'Semua Kandang') {
paramsText.push(`Kandang: ${params.kandang_name}`);
}
if (params.period) {
const formattedDate = formatDate(params.period, 'DD MMM YYYY');
paramsText.push(`Tanggal: ${formattedDate}`);
}
if (params.weight_min || params.weight_max) {
const weightRange =
params.weight_min && params.weight_max
? `${params.weight_min} - ${params.weight_max} kg`
: params.weight_min
? `${params.weight_min} kg`
: `${params.weight_max} kg`;
paramsText.push(`Rentang Bobot: ${weightRange}`);
}
if (params.show_unrecorded === 'true') {
paramsText.push('Tampilkan: Tanpa Recording');
}
const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
paramsText.push(`Dicetak: ${currentDate}`);
return paramsText;
}; };
// Helper functions for PdfTable - Rekapitulasi // Helper functions for PdfTable - Rekapitulasi
@@ -133,65 +80,79 @@ const getRekapitulasiColumns = (): PdfColumn[] => [
flex: 1.2, flex: 1.2,
align: 'right', align: 'right',
}, },
{ key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.5, align: 'left' }, {
{ key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1.2, align: 'left' }, key: 'feed_supplier',
header: 'Feed (Supplier)',
flex: 1.5,
align: 'left',
},
{
key: 'doc_supplier',
header: 'DOC (Supplier)',
flex: 1.2,
align: 'left',
},
{ {
key: 'rata_harga_doc', key: 'rata_harga_doc',
header: 'Rata-Rata Harga DOC', header: 'Rata-Rata Harga DOC',
flex: 1.2, flex: 1.2,
align: 'right', align: 'right',
}, },
{ key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1.2, align: 'right' }, {
{ key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, key: 'hpp_telur',
header: 'HPP Telur (Rp/Kg)',
flex: 1.2,
align: 'right',
},
{
key: 'nominal_sisa',
header: 'Nominal Sisa',
flex: 1.2,
align: 'right',
},
]; ];
const getRekapitulasiData = ( const getRekapitulasiData = (
perWeightRange: HppPerKandangPerWeightRange[] perWeightRange: HppPerKandangPerWeightRange[]
): PdfTbodyCell[][] => { ): PdfTbodyCell[][] => {
return perWeightRange.map((group) => [ return perWeightRange.map((group) => [
{ key: 'rentang_bw', value: group.label, align: 'center' }, { key: 'rentang_bw', value: group.label || '-' },
{ {
key: 'sisa_butir', key: 'sisa_butir',
value: formatNumber(group.egg_production_pieces), value: formatNumber(group.egg_production_pieces || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'sisa_kg', key: 'sisa_kg',
value: formatNumber(group.egg_production_kg), value: formatNumber(group.egg_production_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'rata_rata_bobot', key: 'rata_rata_bobot',
value: formatNumber(group.avg_weight_kg), value: formatNumber(group.avg_weight_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'feed_supplier', key: 'feed_supplier',
value: value: formatSuppliers(group.feed_suppliers),
group.feed_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-',
}, },
{ {
key: 'doc_supplier', key: 'doc_supplier',
value: value: formatSuppliers(group.doc_suppliers),
group.doc_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-',
}, },
{ {
key: 'rata_harga_doc', key: 'rata_harga_doc',
value: formatCurrency(group.average_doc_price_rp), value: formatCurrency(group.average_doc_price_rp || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'hpp_telur', key: 'hpp_telur',
value: formatCurrency(group.egg_hpp_rp_per_kg), value: formatCurrency(group.egg_hpp_rp_per_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'nominal_sisa', key: 'nominal_sisa',
value: formatCurrency(group.egg_value_rp), value: formatCurrency(group.egg_value_rp || 0),
align: 'right', align: 'right',
}, },
]); ]);
@@ -210,21 +171,45 @@ const getDetailColumns = (): PdfColumn[] => [
}, },
{ key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' }, { key: 'sisa_butir', header: 'Sisa Butir', flex: 0.8, align: 'right' },
{ key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' }, { key: 'sisa_kg', header: 'Sisa Kg (Telur)', flex: 0.8, align: 'right' },
{ key: 'feed_supplier', header: 'Feed (Supplier)', flex: 1.2, align: 'left' }, {
{ key: 'doc_supplier', header: 'DOC (Supplier)', flex: 1, align: 'left' }, key: 'feed_supplier',
header: 'Feed (Supplier)',
flex: 1.2,
align: 'left',
},
{
key: 'doc_supplier',
header: 'DOC (Supplier)',
flex: 1,
align: 'left',
},
{ {
key: 'rata_harga_doc', key: 'rata_harga_doc',
header: 'Rata-Rata Harga DOC', header: 'Rata-Rata Harga DOC',
flex: 1.2, flex: 1.2,
align: 'right', align: 'right',
}, },
{ key: 'hpp_telur', header: 'HPP Telur (RP/KG)', flex: 1, align: 'right' }, {
{ key: 'nominal_sisa', header: 'Nominal Sisa', flex: 1.2, align: 'right' }, key: 'hpp_telur',
header: 'HPP Telur (Rp/Kg)',
flex: 1,
align: 'right',
},
{
key: 'nominal_sisa',
header: 'Nominal Sisa',
flex: 1.2,
align: 'right',
},
]; ];
const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => { const getDetailData = (
rows: HppPerKandangRow[],
allFeedSuppliers: string,
allDocSuppliers: string
): PdfTbodyCell[][] => {
return rows.map((item, index) => [ return rows.map((item, index) => [
{ key: 'no', value: index + 1, align: 'center' }, { key: 'no', value: index + 1 },
{ key: 'kandang', value: item.kandang?.name || '-' }, { key: 'kandang', value: item.kandang?.name || '-' },
{ {
key: 'rentang_bw', key: 'rentang_bw',
@@ -232,131 +217,146 @@ const getDetailData = (rows: HppPerKandangRow[]): PdfTbodyCell[][] => {
}, },
{ {
key: 'rata_rata_bobot', key: 'rata_rata_bobot',
value: formatNumber(item.avg_weight_kg), value: formatNumber(item.avg_weight_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'sisa_butir', key: 'sisa_butir',
value: formatNumber(item.egg_production_pieces), value: formatNumber(item.egg_production_pieces || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'sisa_kg', key: 'sisa_kg',
value: formatNumber(item.egg_production_kg), value: formatNumber(item.egg_production_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'feed_supplier', key: 'feed_supplier',
value: value: formatSuppliers(item.feed_suppliers),
item.feed_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-',
}, },
{ {
key: 'doc_supplier', key: 'doc_supplier',
value: value: formatSuppliers(item.doc_suppliers),
item.doc_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-',
}, },
{ {
key: 'rata_harga_doc', key: 'rata_harga_doc',
value: formatCurrency(item.average_doc_price_rp), value: formatCurrency(item.average_doc_price_rp || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'hpp_telur', key: 'hpp_telur',
value: formatCurrency(item.egg_hpp_rp_per_kg), value: formatCurrency(item.egg_hpp_rp_per_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'nominal_sisa', key: 'nominal_sisa',
value: formatCurrency(item.egg_value_rp), value: formatCurrency(item.egg_value_rp || 0),
align: 'right', align: 'right',
}, },
]); ]);
}; };
const getDetailFooter = ( const getDetailFooter = (
summary: HppPerKandangReport['summary'] summary: HppPerKandangReport['summary'],
allFeedSuppliers: string,
allDocSuppliers: string
): PdfTfootCell[] => { ): PdfTfootCell[] => {
if (!summary?.total) return []; if (!summary?.total) return [];
const allFeedSuppliers =
summary.total.feed_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-';
const allDocSuppliers =
summary.total.doc_suppliers
?.map((s: { alias?: string; name: string }) => s.alias || s.name)
.join(' | ') || '-';
return [ return [
{ key: 'no', value: 'TOTAL' }, { key: 'no', value: 'TOTAL' },
{ key: 'kandang', value: 'ALL' }, { key: 'kandang', value: 'ALL' },
{ key: 'rentang_bw', value: '-' }, { key: 'rentang_bw', value: '-' },
{ {
key: 'rata_rata_bobot', key: 'rata_rata_bobot',
value: formatNumber(summary.total.average_weight_kg), value: formatNumber(summary.total.average_weight_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'sisa_butir', key: 'sisa_butir',
value: formatNumber(summary.total.total_egg_production_pieces), value: formatNumber(summary.total.total_egg_production_pieces || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'sisa_kg', key: 'sisa_kg',
value: formatNumber(summary.total.total_egg_production_kg), value: formatNumber(summary.total.total_egg_production_kg || 0),
align: 'right', align: 'right',
}, },
{ key: 'feed_supplier', value: allFeedSuppliers }, { key: 'feed_supplier', value: allFeedSuppliers },
{ key: 'doc_supplier', value: allDocSuppliers }, { key: 'doc_supplier', value: allDocSuppliers },
{ {
key: 'rata_harga_doc', key: 'rata_harga_doc',
value: formatCurrency(summary.total.total_average_doc_price_rp), value: formatCurrency(summary.total.total_average_doc_price_rp || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'hpp_telur', key: 'hpp_telur',
value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg), value: formatCurrency(summary.total.average_egg_hpp_rp_per_kg || 0),
align: 'right', align: 'right',
}, },
{ {
key: 'nominal_sisa', key: 'nominal_sisa',
value: formatCurrency(summary.total.total_egg_value_rp), value: formatCurrency(summary.total.total_egg_value_rp || 0),
align: 'right', align: 'right',
}, },
]; ];
}; };
const createPDFDocument = ( const createPDFDocument = (
data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams,
params: HppPerKandangExportParams['params'] allFeedSuppliers: string,
allDocSuppliers: string
) => { ) => {
const rekapitulasiByWeightRange = data.summary?.per_weight_range || []; const rekapitulasiByWeightRange = params.data.summary?.per_weight_range || [];
const weightRangeText =
params.params?.weight_min || params.params?.weight_max
? params.params.weight_min && params.params.weight_max
? `${params.params.weight_min} - ${params.params.weight_max} kg`
: params.params.weight_min
? `${params.params.weight_min} kg`
: `${params.params.weight_max} kg`
: '-';
return ( return (
<Document> <Document>
<Page size='A3' orientation='landscape' style={pdfStyles.page}> <Page size='A3' orientation='landscape' style={pdfStyles.page}>
{/* Title and Parameters */} {/* Title and Parameters */}
<View style={pdfStyles.titleSection}> <View style={pdfStyles.titleSection}>
<Text style={pdfStyles.mainTitle}> <PdfTypography size='h1' variant='primary'>
Laporan &gt; HPP Harian Kandang Laporan &gt; HPP Harian Kandang
</Text> </PdfTypography>
<View style={pdfStyles.parameterContainer}> <View style={pdfStyles.parameterContainer}>
{getParameterText(params).map((param, index) => ( <PdfParamBadge>
<View key={index} style={pdfStyles.parameterBadge}> Area: {params.params?.area_name || 'Semua Area'}
<Text>{param}</Text> </PdfParamBadge>
</View> <PdfParamBadge>
))} Lokasi: {params.params?.location_name || 'Semua Lokasi'}
</PdfParamBadge>
<PdfParamBadge>
Kandang: {params.params?.kandang_name || 'Semua Kandang'}
</PdfParamBadge>
<PdfParamBadge>
Periode:{' '}
{params.params?.period
? formatDate(params.params.period, 'DD MMM YYYY')
: '-'}
</PdfParamBadge>
<PdfParamBadge>Rentang Bobot: {weightRangeText}</PdfParamBadge>
{params.params?.show_unrecorded === 'true' && (
<PdfParamBadge>Tampilkan: Tanpa Recording</PdfParamBadge>
)}
<PdfParamBadge>
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
</PdfParamBadge>
</View> </View>
</View> </View>
{/* Rekapitulasi Section */} {/* Rekapitulasi Section */}
<View style={pdfStyles.section}> <View style={pdfStyles.section}>
<Text style={pdfStyles.supplierTitle}>Rekapitulasi</Text> <PdfTypography size='h2' variant='primary'>
Rekapitulasi
</PdfTypography>
<PdfTable <PdfTable
columns={getRekapitulasiColumns()} columns={getRekapitulasiColumns()}
data={getRekapitulasiData(rekapitulasiByWeightRange)} data={getRekapitulasiData(rekapitulasiByWeightRange)}
@@ -365,11 +365,25 @@ const createPDFDocument = (
{/* Detail Per Kandang Section */} {/* Detail Per Kandang Section */}
<View style={pdfStyles.section}> <View style={pdfStyles.section}>
<Text style={pdfStyles.supplierTitle}>Detail Per Kandang</Text> <PdfTypography size='h2' variant='primary'>
Detail Per Kandang
</PdfTypography>
<PdfTable <PdfTable
columns={getDetailColumns()} columns={getDetailColumns()}
data={getDetailData(data.rows)} data={getDetailData(
footer={data.summary ? getDetailFooter(data.summary) : undefined} params.data.rows,
allFeedSuppliers,
allDocSuppliers
)}
footer={
params.data.summary
? getDetailFooter(
params.data.summary,
allFeedSuppliers,
allDocSuppliers
)
: undefined
}
footerLabel='TOTAL' footerLabel='TOTAL'
/> />
</View> </View>
@@ -379,10 +393,15 @@ const createPDFDocument = (
}; };
export const generateHppPerKandangPDF = async ( export const generateHppPerKandangPDF = async (
data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams,
params: HppPerKandangExportParams['params'] allFeedSuppliers: string,
allDocSuppliers: string
): Promise<void> => { ): Promise<void> => {
const PDFDocument = createPDFDocument(data, params); const PDFDocument = createPDFDocument(
params,
allFeedSuppliers,
allDocSuppliers
);
try { try {
const blob = await pdf(PDFDocument).toBlob(); const blob = await pdf(PDFDocument).toBlob();
@@ -390,7 +409,8 @@ export const generateHppPerKandangPDF = async (
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
const period = params.period || formatDate(new Date(), 'YYYY-MM-DD'); const period =
params.params?.period || formatDate(new Date(), 'YYYY-MM-DD');
link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`; link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`;
document.body.appendChild(link); document.body.appendChild(link);
@@ -406,16 +406,23 @@ const HppPerKandangTab = () => {
.join(', ') || 'Semua Kandang' .join(', ') || 'Semua Kandang'
: 'Semua Kandang'; : 'Semua Kandang';
await generateHppPerKandangPDF(allDataForExport, { await generateHppPerKandangPDF(
area_name: areaName, {
location_name: locationName, data: allDataForExport,
kandang_name: kandangName, params: {
period: tableFilterState.period, area_name: areaName,
weight_min: tableFilterState.weight_min, location_name: locationName,
weight_max: tableFilterState.weight_max, kandang_name: kandangName,
show_unrecorded: tableFilterState.show_unrecorded.toString(), period: tableFilterState.period,
sort_by: tableFilterState.sort_by, weight_min: tableFilterState.weight_min,
}); weight_max: tableFilterState.weight_max,
show_unrecorded: tableFilterState.show_unrecorded.toString(),
sort_by: tableFilterState.sort_by,
},
},
allFeedSuppliers,
allDocSuppliers
);
toast.success('PDF berhasil dibuat dan diunduh.'); toast.success('PDF berhasil dibuat dan diunduh.');
} catch { } catch {
@@ -429,6 +436,8 @@ const HppPerKandangTab = () => {
areaOptions, areaOptions,
locationOptions, locationOptions,
kandangOptions, kandangOptions,
allFeedSuppliers,
allDocSuppliers,
]); ]);
const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => { const getTableColumns = (): ColumnDef<HppPerKandangReport['rows'][0]>[] => {