diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx
new file mode 100644
index 00000000..dc0c95f0
--- /dev/null
+++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx
@@ -0,0 +1,490 @@
+'use client';
+
+import {
+ Page,
+ Text,
+ View,
+ Document,
+ StyleSheet,
+ Font,
+ pdf,
+} from '@react-pdf/renderer';
+import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang';
+import { formatDate, formatNumber } from '@/lib/helper';
+
+Font.register({
+ family: 'Helvetica',
+ src: 'helvetica',
+});
+
+const pdfStyles = StyleSheet.create({
+ page: {
+ fontSize: 10,
+ fontFamily: 'Helvetica',
+ padding: 20,
+ backgroundColor: '#FFFFFF',
+ },
+ titleSection: {
+ marginBottom: 10,
+ },
+ mainTitle: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ marginBottom: 5,
+ color: '#1f74bf',
+ },
+ supplierTitle: {
+ fontSize: 12,
+ fontWeight: 'bold',
+ marginBottom: 8,
+ color: '#1f74bf',
+ },
+ 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: 8,
+ textAlign: 'left',
+ },
+ tableCellHeader: {
+ flex: 1,
+ borderRightWidth: 1,
+ borderRightColor: '#000000',
+ borderRightStyle: 'solid',
+ padding: 4,
+ fontSize: 8,
+ 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: 8,
+ 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: 8,
+ textAlign: 'right',
+ },
+ tableCellCenter: {
+ flex: 1,
+ borderRightWidth: 1,
+ borderRightColor: '#000000',
+ borderRightStyle: 'solid',
+ padding: 4,
+ fontSize: 8,
+ textAlign: 'center',
+ },
+ tableBorderBottom: {
+ borderBottomWidth: 1,
+ borderBottomColor: '#000000',
+ borderBottomStyle: 'solid',
+ },
+ supplierSection: {
+ marginBottom: 10,
+ },
+ supplierSectionBreak: {
+ marginBottom: 15,
+ },
+ parameterBadge: {
+ backgroundColor: '#F5F5F5',
+ color: '#333333',
+ padding: 4,
+ borderRadius: 4,
+ fontSize: 8,
+ marginRight: 8,
+ marginBottom: 4,
+ },
+ parameterContainer: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ marginBottom: 8,
+ },
+});
+
+interface HppPerKandangExportParams {
+ data: HppPerKandangReport;
+ params: {
+ area_name?: string;
+ location_name?: string;
+ kandang_name?: string;
+ period?: string;
+ weight_min?: string;
+ weight_max?: string;
+ show_unrecorded?: string;
+ sort_by?: string;
+ };
+}
+
+interface WeightRangeGroup {
+ weight_min: number;
+ weight_max: number;
+ items: HppPerKandangReport['rows'];
+ totals: {
+ total_remaining_chicken_birds: number;
+ total_remaining_chicken_weight_kg: number;
+ average_weight_kg: number;
+ total_hpp_rp: number;
+ total_remaining_value_rp: number;
+ all_feed_suppliers: string[];
+ all_doc_suppliers: string[];
+ average_doc_price_rp: number;
+ };
+}
+
+const groupDataByWeightRange = (
+ data: HppPerKandangReport['rows']
+): WeightRangeGroup[] => {
+ const groups: {
+ [key: string]: WeightRangeGroup;
+ } = {};
+
+ data.forEach((item) => {
+ const key = `${item.weight_range.weight_min}-${item.weight_range.weight_max}`;
+ if (!groups[key]) {
+ groups[key] = {
+ weight_min: item.weight_range.weight_min,
+ weight_max: item.weight_range.weight_max,
+ items: [],
+ totals: {
+ total_remaining_chicken_birds: 0,
+ total_remaining_chicken_weight_kg: 0,
+ average_weight_kg: 0,
+ total_hpp_rp: 0,
+ total_remaining_value_rp: 0,
+ all_feed_suppliers: [],
+ all_doc_suppliers: [],
+ average_doc_price_rp: 0,
+ },
+ };
+ }
+
+ groups[key].items.push(item);
+
+ groups[key].totals.total_remaining_chicken_birds +=
+ item.remaining_chicken_birds;
+ groups[key].totals.total_remaining_chicken_weight_kg +=
+ item.remaining_chicken_weight_kg;
+ groups[key].totals.total_hpp_rp += item.hpp_rp;
+ groups[key].totals.total_remaining_value_rp += item.remaining_value_rp;
+
+ item.feed_suppliers?.forEach((supplier) => {
+ const alias = supplier.alias || supplier.name;
+ if (!groups[key].totals.all_feed_suppliers.includes(alias)) {
+ groups[key].totals.all_feed_suppliers.push(alias);
+ }
+ });
+
+ item.doc_suppliers?.forEach((supplier) => {
+ const alias = supplier.alias || supplier.name;
+ if (!groups[key].totals.all_doc_suppliers.includes(alias)) {
+ groups[key].totals.all_doc_suppliers.push(alias);
+ }
+ });
+ });
+
+ Object.values(groups).forEach((group) => {
+ group.totals.average_weight_kg =
+ group.totals.total_remaining_chicken_weight_kg /
+ group.totals.total_remaining_chicken_birds;
+ group.totals.average_doc_price_rp =
+ group.items.reduce((sum, item) => sum + item.average_doc_price_rp, 0) /
+ group.items.length;
+ });
+
+ return Object.values(groups).sort((a, b) => a.weight_min - b.weight_min);
+};
+
+const getParameterText = (params: HppPerKandangExportParams['params']) => {
+ const paramsText = [];
+
+ if (params.period) {
+ const formattedDate = formatDate(params.period, 'DD MMM YYYY');
+ paramsText.push(`Tanggal: ${formattedDate}`);
+ }
+
+ const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm');
+ paramsText.push(`Dicetak: ${currentDate}`);
+
+ return paramsText;
+};
+
+const createPDFDocument = (
+ data: HppPerKandangExportParams['data'],
+ params: HppPerKandangExportParams['params']
+) => {
+ const groupedByWeightRange = groupDataByWeightRange(data.rows);
+
+ return (
+
+
+ {/* Title and Parameters */}
+
+
+ Laporan > HPP Harian Kandang
+
+
+ {getParameterText(params).map((param, index) => (
+
+ {param}
+
+ ))}
+
+
+
+ {/* Rekapitulasi Section */}
+
+ Rekapitulasi
+
+
+ {/* Table Header */}
+
+
+ Rentang BW
+
+
+ Sisa Ekor
+
+
+ Sisa Kg
+
+
+ Rata-Rata Bobot (Kg)
+
+
+ Feed (Supplier)
+
+
+ DOC (Supplier)
+
+
+ Rata-Rata Harga DOC
+
+
+ HPP
+
+
+ Nominal Sisa
+
+
+
+ {/* Table Body - Rekapitulasi */}
+ {groupedByWeightRange.map((group, index) => (
+
+
+
+ {group.weight_min.toFixed(2)} -{' '}
+ {group.weight_max.toFixed(2)}
+
+
+
+
+ {formatNumber(group.totals.total_remaining_chicken_birds)}
+
+
+
+
+ {formatNumber(
+ group.totals.total_remaining_chicken_weight_kg
+ )}
+
+
+
+ {formatNumber(group.totals.average_weight_kg)}
+
+
+ {group.totals.all_feed_suppliers.join(' | ')}
+
+
+ {group.totals.all_doc_suppliers.join(' | ')}
+
+
+ {formatNumber(group.totals.average_doc_price_rp)}
+
+
+
+ {
+ (group.totals.total_remaining_chicken_birds > 0
+ ? group.totals.total_hpp_rp /
+ group.totals.total_remaining_chicken_birds
+ : 0,
+ 2)
+ }
+
+
+
+
+ {formatNumber(group.totals.total_remaining_value_rp)}
+
+
+
+ ))}
+
+
+
+ {/* Detail Per Kandang Section */}
+
+ Detail Per Kandang
+
+
+ {/* Table Header */}
+
+
+ No
+
+
+ Kandang
+
+
+ Rentang BW
+
+
+ Sisa Ekor
+
+
+ Sisa Kg
+
+
+ Rata-Rata Bobot (Kg)
+
+
+ Feed (Supplier)
+
+
+ DOC (Supplier)
+
+
+ Rata-Rata Harga DOC
+
+
+ HPP
+
+
+ Nominal Sisa
+
+
+
+ {/* Table Body - Detail Per Kandang */}
+ {data.rows.map((item, index) => (
+
+
+ {index + 1}
+
+
+ {item.kandang?.name || '-'}
+
+
+
+ {item.weight_range.weight_min.toFixed(2)} -{' '}
+ {item.weight_range.weight_max.toFixed(2)}
+
+
+
+ {formatNumber(item.remaining_chicken_birds)}
+
+
+ {formatNumber(item.remaining_chicken_weight_kg)}
+
+
+ {formatNumber(item.avg_weight_kg)}
+
+
+
+ {item.feed_suppliers
+ ?.map((s) => s.alias || s.name)
+ .join(' | ')}
+
+
+
+
+ {item.doc_suppliers
+ ?.map((s) => s.alias || s.name)
+ .join(' | ')}
+
+
+
+ {formatNumber(item.average_doc_price_rp)}
+
+
+ {formatNumber(item.hpp_rp)}
+
+
+ {formatNumber(item.remaining_value_rp)}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export const generateHppPerKandangPDF = async (
+ data: HppPerKandangExportParams['data'],
+ params: HppPerKandangExportParams['params']
+): Promise => {
+ const PDFDocument = createPDFDocument(data, params);
+
+ try {
+ const blob = await pdf(PDFDocument).toBlob();
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `laporan-hpp-harian-kandang-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.pdf`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ } catch (error) {
+ throw error;
+ }
+};
diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
index 0481dd2a..d90040c6 100644
--- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
+++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
@@ -23,6 +23,7 @@ import Button from '@/components/Button';
import Dropdown from '@/components/dropdown/Dropdown';
import MenuItem from '@/components/menu/MenuItem';
import Menu from '@/components/menu/Menu';
+import { generateHppPerKandangPDF } from '../export/HppPerkandangExport';
import toast from 'react-hot-toast';
import * as XLSX from 'xlsx';
@@ -380,6 +381,54 @@ const HppPerKandangTab = () => {
kandangOptions,
]);
+ const handleExportPDF = useCallback(async () => {
+ if (!hppPerKandang || !isResponseSuccess(hppPerKandang)) {
+ toast.error('Tidak ada data untuk diekspor.');
+ return;
+ }
+
+ try {
+ const areaName = tableFilterState.area_id
+ ? areaOptions.find(
+ (opt) => opt.value === Number(tableFilterState.area_id)
+ )?.label || 'Semua Area'
+ : 'Semua Area';
+
+ const locationName = tableFilterState.location_id
+ ? locationOptions.find(
+ (opt) => opt.value === Number(tableFilterState.location_id)
+ )?.label || 'Semua Lokasi'
+ : 'Semua Lokasi';
+
+ const kandangName = tableFilterState.kandang_id
+ ? kandangOptions.find(
+ (opt) => opt.value === Number(tableFilterState.kandang_id)
+ )?.label || 'Semua Kandang'
+ : 'Semua Kandang';
+
+ await generateHppPerKandangPDF(hppPerKandang.data, {
+ area_name: areaName,
+ location_name: locationName,
+ kandang_name: kandangName,
+ period: tableFilterState.period,
+ weight_min: tableFilterState.weight_min,
+ weight_max: tableFilterState.weight_max,
+ show_unrecorded: tableFilterState.show_unrecorded.toString(),
+ sort_by: tableFilterState.sort_by,
+ });
+
+ toast.success('PDF berhasil dibuat dan diunduh.');
+ } catch {
+ toast.error('Gagal membuat PDF. Silakan coba lagi.');
+ }
+ }, [
+ hppPerKandang,
+ tableFilterState,
+ areaOptions,
+ locationOptions,
+ kandangOptions,
+ ]);
+
// ===== TABLE COLUMNS DEFINITION =====
const totals: Totals = useMemo(() => {
return {
@@ -592,12 +641,7 @@ const HppPerKandangTab = () => {
>