From 1227b7639ff87983518152f403346b07776ca9f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 10 Feb 2026 16:52:52 +0700 Subject: [PATCH] refactor(FE): Refactor ProductionResultReportPDF to use reusable PDF components --- .../ProductionResultReportPDF.tsx | 679 ++++++++++-------- 1 file changed, 376 insertions(+), 303 deletions(-) diff --git a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx index 9bc27c4b..139f4640 100644 --- a/src/components/pages/report/production-result/ProductionResultReportPDF.tsx +++ b/src/components/pages/report/production-result/ProductionResultReportPDF.tsx @@ -1,18 +1,19 @@ 'use client'; import React from 'react'; -import { - Document, - Page, - StyleSheet, - Text, - View, - Image, -} from '@react-pdf/renderer'; +import { Document, Page, StyleSheet, View, Text } from '@react-pdf/renderer'; import { formatDate, formatNumber } from '@/lib/helper'; import { BaseProjectFlockKandang } from '@/types/api/production/project-flock-kandang'; import { ProductionResult } from '@/types/api/report/production-result'; +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, + PdfTbodyCell, +} from '@/components/helper/pdf/table'; type MappedProductionResultsItem = { projectFlockKandang: BaseProjectFlockKandang; @@ -25,132 +26,28 @@ interface ProductionResultReportPDFProps { const styles = StyleSheet.create({ page: { - paddingTop: 24, - paddingBottom: 52, - paddingHorizontal: 16, - }, - - companyInfoHeader: { - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - marginBottom: 8, - }, - companyLogo: { - width: 64, - height: 'auto', - }, - companyInfoHeaderDate: { - paddingTop: 8, fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', }, - companyName: { - fontSize: 12, - fontWeight: 'bold', - marginBottom: 4, - }, - companyAddress: { - fontSize: 8, - maxWidth: 420, + titleSection: { marginBottom: 10, }, - doubleDivider: { - width: '100%', - height: 6, - borderTopWidth: 2, - borderTopColor: '#000', - borderBottomWidth: 2, - borderBottomColor: '#000', - }, - - title: { - marginTop: 14, - fontSize: 14, - lineHeight: '150%', - textAlign: 'center', - fontFamily: 'Times-Roman', - fontWeight: 'bold', - }, - - footer: { - width: '100%', - display: 'flex', + parameterContainer: { flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 16, - position: 'absolute', - fontSize: 8, - bottom: 22, - left: 0, - right: 0, - textAlign: 'center', - color: 'grey', + flexWrap: 'wrap', + marginBottom: 8, }, - - section: { - marginTop: 12, - borderWidth: 1, - borderColor: '#000', - padding: 8, + tableSection: { + marginBottom: 12, }, - - sectionHeader: { - marginBottom: 6, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'baseline', - }, - sectionTitle: { + tableTitle: { fontSize: 10, fontWeight: 'bold', + marginBottom: 6, + color: '#333', }, - sectionSubtitle: { - fontSize: 8, - color: '#444', - }, - - // Simple grid table (label/value pairs) - grid: { - width: '100%', - borderWidth: 1, - borderColor: '#000', - }, - gridRow: { - flexDirection: 'row', - borderBottomWidth: 1, - borderBottomColor: '#000', - }, - gridRowLast: { - borderBottomWidth: 0, - }, - gridCellLabel: { - width: '40%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - borderRightWidth: 1, - borderRightColor: '#000', - fontWeight: 'bold', - }, - gridCellValue: { - width: '60%', - paddingVertical: 3, - paddingHorizontal: 6, - fontSize: 8, - textAlign: 'right', - }, - - // Subsection headings - groupTitle: { - marginTop: 8, - marginBottom: 4, - fontSize: 9, - fontWeight: 'bold', - }, - emptyText: { fontSize: 8, color: '#666', @@ -169,125 +66,243 @@ function valueText(v: unknown) { return String(v); } -/** - * Render label/value table for one ProductionResult. - * Uses a compact grid to keep page readable. - */ -function ProductionResultGrid({ pr }: { pr: ProductionResult }) { - const rows: Array<[string, string]> = [ - ['WOA', valueText(pr.woa)], +// ======================================== +// TABLE 1: WOA & BW +// ======================================== +const getBwTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'woa', header: 'WOA', flex: 0.8, align: 'center' }, + { key: 'bw', header: 'BW', flex: 1, align: 'right' }, + { key: 'std_bw', header: 'Std BW', flex: 1, align: 'right' }, + { key: 'uniformity', header: 'Uniformity', flex: 1.2, align: 'right' }, + { + key: 'std_uniformity', + header: 'Std Uniformity', + flex: 1.3, + align: 'right', + }, +]; - // BW - ['BW', valueText(pr.bw)], - ['Std BW', valueText(pr.std_bw)], - ['Uniformity', valueText(pr.uniformity)], - ['Std Uniformity', valueText(pr.std_uniformity)], +const getBwTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'woa', value: valueText(pr.woa) }, + { key: 'bw', value: valueText(pr.bw), align: 'right' }, + { key: 'std_bw', value: valueText(pr.std_bw), align: 'right' }, + { key: 'uniformity', value: valueText(pr.uniformity), align: 'right' }, + { + key: 'std_uniformity', + value: valueText(pr.std_uniformity), + align: 'right', + }, + ]; + }); +}; - // Dep - ['Dep Kum', valueText(pr.dep_kum)], - ['Dep Std', valueText(pr.dep_std)], +// ======================================== +// TABLE 2: DEPLESI +// ======================================== +const getDepTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'dep_kum', header: 'Dep Kum', flex: 1.5, align: 'right' }, + { key: 'dep_std', header: 'Dep Std', flex: 1.5, align: 'right' }, +]; - // Butiran - ['Butiran Utuh', valueText(pr.butiran_utuh)], - ['Butiran Putih', valueText(pr.butiran_putih)], - ['Butiran Retak', valueText(pr.butiran_retak)], - ['Butiran Pecah', valueText(pr.butiran_pecah)], - ['Butiran Jumlah', valueText(pr.butiran_jumlah)], - ['Total Butir', valueText(pr.total_butir)], +const getDepTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'dep_kum', value: valueText(pr.dep_kum), align: 'right' }, + { key: 'dep_std', value: valueText(pr.dep_std), align: 'right' }, + ]; + }); +}; - // Kg - ['Kg Utuh', valueText(pr.kg_utuh)], - ['Kg Putih', valueText(pr.kg_putih)], - ['Kg Retak', valueText(pr.kg_retak)], - ['Kg Pecah', valueText(pr.kg_pecah)], - ['Kg Jumlah', valueText(pr.kg_jumlah)], - ['Total Kg', valueText(pr.total_kg)], +// ======================================== +// TABLE 3: BUTIRAN +// ======================================== +const getButiranTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'butiran_utuh', header: 'Utuh', flex: 1.2, align: 'right' }, + { key: 'butiran_putih', header: 'Putih', flex: 1.2, align: 'right' }, + { key: 'butiran_retak', header: 'Retak', flex: 1.2, align: 'right' }, + { key: 'butiran_pecah', header: 'Pecah', flex: 1.2, align: 'right' }, + { key: 'butiran_jumlah', header: 'Jumlah', flex: 1.2, align: 'right' }, + { key: 'total_butir', header: 'Total Butir', flex: 1.3, align: 'right' }, +]; - // % - ['% Utuh', valueText(pr.persen_utuh)], - ['% Putih', valueText(pr.persen_putih)], - ['% Retak', valueText(pr.persen_retak)], - ['% Pecah', valueText(pr.persen_pecah)], +const getButiranTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'butiran_utuh', + value: valueText(pr.butiran_utuh), + align: 'right', + }, + { + key: 'butiran_putih', + value: valueText(pr.butiran_putih), + align: 'right', + }, + { + key: 'butiran_retak', + value: valueText(pr.butiran_retak), + align: 'right', + }, + { + key: 'butiran_pecah', + value: valueText(pr.butiran_pecah), + align: 'right', + }, + { + key: 'butiran_jumlah', + value: valueText(pr.butiran_jumlah), + align: 'right', + }, + { + key: 'total_butir', + value: valueText(pr.total_butir), + align: 'right', + }, + ]; + }); +}; - // Produksi - ['HD', valueText(pr.hd)], - ['HD Std', valueText(pr.hd_std)], - ['FI', valueText(pr.fi)], - ['FI Std', valueText(pr.fi_std)], - ['EM', valueText(pr.em)], - ['EM Std', valueText(pr.em_std)], - ['EW', valueText(pr.ew)], - ['EW Std', valueText(pr.ew_std)], - ['FCR', valueText(pr.fcr)], - ['FCR Std', valueText(pr.fcr_std)], - ['HH', valueText(pr.hh)], - ['HH Std', valueText(pr.hh_std)], - ]; +// ======================================== +// TABLE 4: BERAT (KG) +// ======================================== +const getKgTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'kg_utuh', header: 'Utuh (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_putih', header: 'Putih (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_retak', header: 'Retak (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_pecah', header: 'Pecah (Kg)', flex: 1.2, align: 'right' }, + { key: 'kg_jumlah', header: 'Jumlah (Kg)', flex: 1.3, align: 'right' }, + { key: 'total_kg', header: 'Total (Kg)', flex: 1.3, align: 'right' }, +]; - return ( - - {rows.map(([label, value], idx) => { - const isLast = idx === rows.length - 1; - return ( - - {label} - {value} - - ); - })} - - ); -} +const getKgTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'kg_utuh', value: valueText(pr.kg_utuh), align: 'right' }, + { key: 'kg_putih', value: valueText(pr.kg_putih), align: 'right' }, + { key: 'kg_retak', value: valueText(pr.kg_retak), align: 'right' }, + { key: 'kg_pecah', value: valueText(pr.kg_pecah), align: 'right' }, + { key: 'kg_jumlah', value: valueText(pr.kg_jumlah), align: 'right' }, + { key: 'total_kg', value: valueText(pr.total_kg), align: 'right' }, + ]; + }); +}; -/** - * If there are multiple ProductionResult entries for a kandang, - * we show them sequentially with a small header per result. - * - * You can later change this to render only the latest WOA, or group by week. - */ -function ProductionResultList({ - productionResults, -}: { - productionResults: ProductionResult[]; -}) { - return ( - - {productionResults.map((pr, idx) => { - const kandangName = - pr.project_flock?.kandang?.name || - pr.project_flock?.kandang?.id?.toString() || - ''; +// ======================================== +// TABLE 5: PERSENTASE +// ======================================== +const getPersenTableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'persen_utuh', header: '% Utuh', flex: 1.5, align: 'right' }, + { key: 'persen_putih', header: '% Putih', flex: 1.5, align: 'right' }, + { key: 'persen_retak', header: '% Retak', flex: 1.5, align: 'right' }, + { key: 'persen_pecah', header: '% Pecah', flex: 1.5, align: 'right' }, +]; - // Optional: show a compact subheader - const headerLeft = `Data #${idx + 1}`; - const headerRight = - kandangName && pr.woa !== undefined - ? `${kandangName} • WOA ${safeNum(pr.woa)}` - : pr.woa !== undefined - ? `WOA ${safeNum(pr.woa)}` - : ''; +const getPersenTableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { + key: 'persen_utuh', + value: valueText(pr.persen_utuh), + align: 'right', + }, + { + key: 'persen_putih', + value: valueText(pr.persen_putih), + align: 'right', + }, + { + key: 'persen_retak', + value: valueText(pr.persen_retak), + align: 'right', + }, + { + key: 'persen_pecah', + value: valueText(pr.persen_pecah), + align: 'right', + }, + ]; + }); +}; - return ( - - - {headerLeft} - {headerRight} - +// ======================================== +// TABLE 6: PRODUKSI (HD, FI, EM, EW) +// ======================================== +const getProduksi1TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'hd', header: 'HD', flex: 0.8, align: 'right' }, + { key: 'hd_std', header: 'HD Std', flex: 1, align: 'right' }, + { key: 'fi', header: 'FI', flex: 0.8, align: 'right' }, + { key: 'fi_std', header: 'FI Std', flex: 1, align: 'right' }, + { key: 'em', header: 'EM', flex: 0.8, align: 'right' }, + { key: 'em_std', header: 'EM Std', flex: 1, align: 'right' }, + { key: 'ew', header: 'EW', flex: 0.8, align: 'right' }, + { key: 'ew_std', header: 'EW Std', flex: 1, align: 'right' }, +]; - - - ); - })} - - ); -} +const getProduksi1TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'hd', value: valueText(pr.hd), align: 'right' }, + { key: 'hd_std', value: valueText(pr.hd_std), align: 'right' }, + { key: 'fi', value: valueText(pr.fi), align: 'right' }, + { key: 'fi_std', value: valueText(pr.fi_std), align: 'right' }, + { key: 'em', value: valueText(pr.em), align: 'right' }, + { key: 'em_std', value: valueText(pr.em_std), align: 'right' }, + { key: 'ew', value: valueText(pr.ew), align: 'right' }, + { key: 'ew_std', value: valueText(pr.ew_std), align: 'right' }, + ]; + }); +}; + +// ======================================== +// TABLE 7: PRODUKSI (FCR, HH) +// ======================================== +const getProduksi2TableColumns = (): PdfColumn[] => [ + { key: 'no', header: 'No', flex: 0.5, align: 'center' }, + { key: 'fcr', header: 'FCR', flex: 1, align: 'right' }, + { key: 'fcr_std', header: 'FCR Std', flex: 1.2, align: 'right' }, + { key: 'hh', header: 'HH', flex: 1, align: 'right' }, + { key: 'hh_std', header: 'HH Std', flex: 1.2, align: 'right' }, +]; + +const getProduksi2TableData = ( + productionResults: ProductionResult[] +): PdfTbodyCell[][] => { + return productionResults.map((pr, index) => { + return [ + { key: 'no', value: index + 1 }, + { key: 'fcr', value: valueText(pr.fcr), align: 'right' }, + { key: 'fcr_std', value: valueText(pr.fcr_std), align: 'right' }, + { key: 'hh', value: valueText(pr.hh), align: 'right' }, + { key: 'hh_std', value: valueText(pr.hh_std), align: 'right' }, + ]; + }); +}; /** * ✅ Main PDF Component @@ -297,90 +312,148 @@ const ProductionResultReportPDF = ({ }: ProductionResultReportPDFProps) => { return ( - - {/* Header */} - - - - - {formatDate(Date.now(), 'DD MMMM YYYY')} - + {mappedProductionResults.length === 0 ? ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + + - - PT LUMBUNG TELUR INDONESIA - - SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. - Cipedes, Kec. Sukajadi, Kota Bandung 40162 - - - - - - - Laporan Production Result - - {/* Sections per ProjectFlockKandang */} - {mappedProductionResults.length === 0 ? ( Tidak ada data. - ) : ( - mappedProductionResults.map((item, idx) => { - const pfk = item.projectFlockKandang; - // Try to display meaningful identifiers. - // Adjust these fields based on your real BaseProjectFlockKandang structure. - const kandangName = - pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; + + + ) : ( + mappedProductionResults.map((item, idx) => { + const pfk = item.projectFlockKandang; - const projectName = pfk?.project_flock?.name ?? ''; + const kandangName = + pfk?.kandang?.name ?? `Kandang #${pfk?.kandang_id ?? idx + 1}`; - const locationName = pfk?.project_flock?.location?.name ?? ''; + const projectName = pfk?.project_flock?.name ?? ''; - const areaName = pfk?.project_flock?.area?.name ?? ''; + const locationName = pfk?.project_flock?.location?.name ?? ''; - return ( - 0} // each kandang starts on a new page for clarity - > - - - {projectName - ? `${projectName} • ${kandangName}` - : kandangName} - - - {[areaName, locationName].filter(Boolean).join(' • ')} - + const areaName = pfk?.project_flock?.area?.name ?? ''; + + const hasData = + item.productionResult && item.productionResult.length > 0; + + return ( + + {/* Title and Parameters */} + + + Laporan > Production Result + + + + Tanggal: {formatDate(Date.now(), 'DD MMMM YYYY')} + + + Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')} + - - {item.productionResult && item.productionResult.length > 0 ? ( - - ) : ( - - Tidak ada production result untuk kandang ini. - - )} + + {projectName + ? `${projectName} • ${kandangName}` + : kandangName} + + + {[areaName, locationName].filter(Boolean).join(' • ')} + - ); - }) - )} - {/* Footer */} - - - `${pageNumber} / ${totalPages}` - } - fixed - /> - - + {hasData ? ( + <> + {/* Table 1: WOA & BW */} + + 1. WOA & Body Weight + + + + {/* Table 2: Deplesi */} + + 2. Deplesi + + + + {/* Table 3: Butiran */} + + 3. Butiran + + + + {/* Table 4: Berat (Kg) */} + + 4. Berat (Kg) + + + + {/* Table 5: Persentase */} + + 5. Persentase + + + + {/* Table 6: Produksi (HD, FI, EM, EW) */} + + + 6. Produksi (HD, FI, EM, EW) + + + + + {/* Table 7: Produksi (FCR, HH) */} + + 7. Produksi (FCR, HH) + + + + ) : ( + + Tidak ada production result untuk kandang ini. + + )} + + + + ); + }) + )} ); };