diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx
index 5c5ee473..ed1ce0ac 100644
--- a/src/components/pages/report/sale/SaleReportTabs.tsx
+++ b/src/components/pages/report/sale/SaleReportTabs.tsx
@@ -1,7 +1,7 @@
'use client';
import Tabs from '@/components/Tabs';
-import { HppPerKandangTab } from '@/components/pages/report/sale/tab/HppPerKandangTab';
+import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTab';
const SaleReportTabs = () => {
const tabs = [
diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
index 60320597..6d72f2d8 100644
--- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
+++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx
@@ -1,3 +1,744 @@
-export const HppPerKandangTab = () => {
- return
HPP Per Kandang Tab
;
+import { useState, useMemo, useCallback } from 'react';
+import { ChangeEventHandler } from 'react';
+import useSWR from 'swr';
+import Card from '@/components/Card';
+import SelectInput, {
+ useSelect,
+ OptionType,
+} from '@/components/input/SelectInput';
+import DateInput from '@/components/input/DateInput';
+import TextInput from '@/components/input/TextInput';
+import CheckboxInput from '@/components/input/CheckboxInput';
+import { AreaApi } from '@/services/api/master-data';
+import { LocationApi } from '@/services/api/master-data';
+import { KandangApi } from '@/services/api/master-data';
+import { SaleReportApi } from '@/services/api/report/marketing-sale';
+import Table from '@/components/Table';
+import { ColumnDef } from '@tanstack/react-table';
+import { formatCurrency, formatNumber } from '@/lib/helper';
+import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang';
+import { isResponseSuccess } from '@/lib/api-helper';
+import { useTableFilter } from '@/services/hooks/useTableFilter';
+import Pagination from '@/components/Pagination';
+import Button from '@/components/Button';
+import Dropdown from '@/components/dropdown/Dropdown';
+import MenuItem from '@/components/menu/MenuItem';
+import Menu from '@/components/menu/Menu';
+import toast from 'react-hot-toast';
+import * as XLSX from 'xlsx';
+
+interface Totals {
+ total_remaining_chicken_birds: number;
+ total_remaining_chicken_weight_kg: number;
+ total_remaining_value_rp: number;
+}
+
+const HppPerKandangTab = () => {
+ // ===== STATE MANAGEMENT =====
+ const [isPdfExportLoading, setIsPdfExportLoading] = useState(false);
+
+ // ===== PAGINATION STATE =====
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+
+ // ===== SUBMISSION STATE =====
+ const [isSubmitted, setIsSubmitted] = useState(false);
+
+ // ===== TABLE FILTER STATE =====
+ const { state: tableFilterState, updateFilter } = useTableFilter({
+ initial: {
+ area_id: '',
+ location_id: '',
+ kandang_id: '',
+ weight_min: '',
+ weight_max: '',
+ period: '',
+ sort_by: '',
+ show_unrecorded: false,
+ },
+ paramMap: {
+ page: 'page',
+ pageSize: 'limit',
+ },
+ });
+
+ const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
+ AreaApi.basePath,
+ 'id',
+ 'name',
+ 'search'
+ );
+
+ const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
+ useSelect(LocationApi.basePath, 'id', 'name', 'search');
+
+ const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
+ useSelect(KandangApi.basePath, 'id', 'name', 'search');
+
+ const areaChangeHandler = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ updateFilter('area_id', newVal?.value ? String(newVal.value) : '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const locationChangeHandler = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ updateFilter('location_id', newVal?.value ? String(newVal.value) : '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const kandangChangeHandler = useCallback(
+ (val: OptionType | OptionType[] | null) => {
+ const newVal = val as OptionType;
+ updateFilter('kandang_id', newVal?.value ? String(newVal.value) : '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const weightMinChangeHandler = useCallback<
+ ChangeEventHandler
+ >(
+ (e) => {
+ const val = e.target.value;
+ updateFilter('weight_min', val || '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const weightMaxChangeHandler = useCallback<
+ ChangeEventHandler
+ >(
+ (e) => {
+ const val = e.target.value;
+ updateFilter('weight_max', val || '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const periodChangeHandler = useCallback>(
+ (e) => {
+ const val = e.target.value;
+ updateFilter('period', val || '');
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const showUnrecordedChangeHandler = useCallback<
+ ChangeEventHandler
+ >(
+ (e) => {
+ const checked = e.target.checked;
+ updateFilter('show_unrecorded', checked);
+ setIsSubmitted(false);
+ },
+ [updateFilter]
+ );
+
+ const resetFilters = useCallback(() => {
+ updateFilter('area_id', '');
+ updateFilter('location_id', '');
+ updateFilter('kandang_id', '');
+ updateFilter('weight_min', '');
+ updateFilter('weight_max', '');
+ updateFilter('period', '');
+ updateFilter('sort_by', '');
+ updateFilter('show_unrecorded', false);
+ setIsSubmitted(false);
+ }, [updateFilter]);
+
+ const handleSubmit = useCallback(() => {
+ if (!tableFilterState.period) {
+ toast.error('Periode wajib diisi');
+ return;
+ }
+ setIsSubmitted(true);
+ setCurrentPage(1);
+ }, [tableFilterState.period]);
+
+ // ===== DATA FETCHING =====
+ const { data: hppPerKandang, isLoading } = useSWR(
+ isSubmitted
+ ? () => {
+ const params = {
+ area_id: tableFilterState.area_id
+ ? Number(tableFilterState.area_id)
+ : undefined,
+ location_id: tableFilterState.location_id
+ ? Number(tableFilterState.location_id)
+ : undefined,
+ kandang_id: tableFilterState.kandang_id
+ ? Number(tableFilterState.kandang_id)
+ : undefined,
+ weight_min: tableFilterState.weight_min
+ ? Number(tableFilterState.weight_min)
+ : undefined,
+ weight_max: tableFilterState.weight_max
+ ? Number(tableFilterState.weight_max)
+ : undefined,
+ period: tableFilterState.period || undefined,
+ sort_by: tableFilterState.sort_by || undefined,
+ show_unrecorded: tableFilterState.show_unrecorded,
+ page: currentPage,
+ limit: pageSize,
+ };
+
+ return ['hpp-per-kandang-report', params];
+ }
+ : null,
+ ([, params]) =>
+ SaleReportApi.getHppPerKandangReport(
+ params.area_id,
+ params.location_id,
+ params.kandang_id,
+ params.weight_min,
+ params.weight_max,
+ params.period,
+ params.sort_by,
+ params.show_unrecorded,
+ params.page,
+ params.limit
+ )
+ );
+
+ const data: HppPerKandangReport['rows'] = useMemo(
+ () =>
+ isResponseSuccess(hppPerKandang)
+ ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || []
+ : [],
+ [hppPerKandang]
+ );
+
+ const summary =
+ isResponseSuccess(hppPerKandang) && 'summary' in hppPerKandang
+ ? hppPerKandang.data.summary
+ : undefined;
+
+ const period =
+ isResponseSuccess(hppPerKandang) && 'period' in hppPerKandang
+ ? hppPerKandang.data.period
+ : undefined;
+
+ const meta =
+ isResponseSuccess(hppPerKandang) && 'meta' in hppPerKandang
+ ? hppPerKandang.meta
+ : undefined;
+
+ const { data: allDataForExport } = useSWR(
+ isSubmitted
+ ? () => {
+ const params = {
+ area_id: tableFilterState.area_id
+ ? Number(tableFilterState.area_id)
+ : undefined,
+ location_id: tableFilterState.location_id
+ ? Number(tableFilterState.location_id)
+ : undefined,
+ kandang_id: tableFilterState.kandang_id
+ ? Number(tableFilterState.kandang_id)
+ : undefined,
+ weight_min: tableFilterState.weight_min
+ ? Number(tableFilterState.weight_min)
+ : undefined,
+ weight_max: tableFilterState.weight_max
+ ? Number(tableFilterState.weight_max)
+ : undefined,
+ period: tableFilterState.period || undefined,
+ sort_by: tableFilterState.sort_by || undefined,
+ show_unrecorded: tableFilterState.show_unrecorded,
+ limit: 10000,
+ page: 1,
+ };
+
+ return ['hpp-per-kandang-report-export', params];
+ }
+ : null,
+ ([, params]) =>
+ SaleReportApi.getHppPerKandangReport(
+ params.area_id,
+ params.location_id,
+ params.kandang_id,
+ params.weight_min,
+ params.weight_max,
+ params.period,
+ params.sort_by,
+ params.show_unrecorded,
+ params.page,
+ params.limit
+ )
+ );
+
+ const allExportData: HppPerKandangReport['rows'] = useMemo(
+ () =>
+ isResponseSuccess(allDataForExport)
+ ? (allDataForExport?.data?.rows as HppPerKandangReport['rows']) || []
+ : [],
+ [allDataForExport]
+ );
+
+ // ===== EXPORT HANDLERS =====
+ const handleExportExcel = useCallback(() => {
+ if (allExportData.length === 0) {
+ toast.error('Tidak ada data untuk diekspor.');
+ return;
+ }
+
+ try {
+ const totals = allExportData.reduce(
+ (acc, item) => ({
+ total_remaining_chicken_birds:
+ acc.total_remaining_chicken_birds +
+ (item.remaining_chicken_birds || 0),
+ total_remaining_chicken_weight_kg:
+ acc.total_remaining_chicken_weight_kg +
+ (item.remaining_chicken_weight_kg || 0),
+ total_remaining_value_rp:
+ acc.total_remaining_value_rp + (item.remaining_value_rp || 0),
+ }),
+ {
+ total_remaining_chicken_birds: 0,
+ total_remaining_chicken_weight_kg: 0,
+ total_remaining_value_rp: 0,
+ }
+ );
+
+ const excelData: { [key: string]: string | number }[] = allExportData.map(
+ (item, index) => ({
+ No: index + 1,
+ Kandang: item.kandang?.name || '',
+ 'Rentang Bobot': item.weight_range
+ ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}`
+ : '',
+ 'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0,
+ 'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0,
+ 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0,
+ 'Feed (Supplier)':
+ item.feed_suppliers?.map((s) => s.alias || s.name).join(' | ') ||
+ '',
+ 'DOC (Supplier)':
+ item.doc_suppliers?.map((s) => s.alias || s.name).join(' | ') || '',
+ 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0,
+ 'HPP (RP)': item.hpp_rp || 0,
+ 'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0,
+ })
+ );
+
+ excelData.push({
+ No: 'TOTAL',
+ Kandang: 'ALL',
+ 'Rentang Bobot': '-',
+ 'Sisa Ayam (Ekor)': totals.total_remaining_chicken_birds,
+ 'Sisa Ayam (KG)': totals.total_remaining_chicken_weight_kg,
+ 'Rata-Rata Bobot (KG)': '',
+ 'Feed (Supplier)': '',
+ 'DOC (Supplier)': '',
+ 'Rata-Rata Harga DOC (RP)': '',
+ 'HPP (RP)': '',
+ 'Nilai Nominal Sisa Ayam (RP)': totals.total_remaining_value_rp,
+ });
+
+ const worksheet = XLSX.utils.json_to_sheet(excelData);
+
+ const colWidths = [
+ { wch: 5 }, // No
+ { wch: 30 }, // Kandang
+ { wch: 15 }, // Rentang Bobot
+ { wch: 15 }, // Sisa Ayam (Ekor)
+ { wch: 15 }, // Sisa Ayam (KG)
+ { wch: 18 }, // Rata-Rata Bobot (KG)
+ { wch: 20 }, // Feed (Supplier)
+ { wch: 20 }, // DOC (Supplier)
+ { wch: 20 }, // Rata-Rata Harga DOC (RP)
+ { wch: 12 }, // HPP (RP)
+ { wch: 25 }, // Nilai Nominal Sisa Ayam (RP)
+ ];
+ worksheet['!cols'] = colWidths;
+
+ const workbook = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang');
+
+ 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';
+
+ const filename = `Laporan_HPP_Per_Kandang_${areaName}_${locationName}_${kandangName}_${tableFilterState.period}.xlsx`;
+
+ XLSX.writeFile(workbook, filename);
+ toast.success('Excel berhasil dibuat dan diunduh.');
+ } catch {
+ toast.error('Gagal membuat Excel. Silakan coba lagi.');
+ }
+ }, [
+ allExportData,
+ tableFilterState,
+ areaOptions,
+ locationOptions,
+ kandangOptions,
+ ]);
+
+ // ===== PAGINATION HANDLERS =====
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ };
+
+ const handleRowChange = (pageSize: number) => {
+ setPageSize(pageSize);
+ };
+
+ const handleNextPage = () => {
+ if (meta && currentPage < meta.total_pages) {
+ setCurrentPage(currentPage + 1);
+ }
+ };
+
+ const handlePrevPage = () => {
+ if (currentPage > 1) {
+ setCurrentPage(currentPage - 1);
+ }
+ };
+
+ // ===== TABLE COLUMNS DEFINITION =====
+ const totals: Totals = useMemo(() => {
+ return {
+ total_remaining_chicken_birds:
+ summary?.total_remaining_chicken_birds || 0,
+ total_remaining_chicken_weight_kg:
+ summary?.total_remaining_chicken_weight_kg || 0,
+ total_remaining_value_rp: summary?.total_remaining_value_rp || 0,
+ };
+ }, [summary]);
+
+ const getTableColumns = (): ColumnDef[] => {
+ const tableColumns: ColumnDef[] = [
+ {
+ id: 'no',
+ header: 'No',
+ cell: (props) => props.row.index + 1,
+ footer: () => TOTAL
,
+ },
+ {
+ id: 'kandang_name',
+ header: 'Kandang',
+ accessorKey: 'kandang.name',
+ cell: (props) => {
+ const kandang = props.row.original.kandang;
+ return kandang?.name || '-';
+ },
+ },
+ {
+ id: 'weight_range',
+ header: 'Rentang Bobot',
+ accessorKey: 'weight_range',
+ cell: (props) => {
+ const weightRange = props.row.original.weight_range;
+ return weightRange
+ ? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}`
+ : '-';
+ },
+ },
+ {
+ id: 'remaining_chicken_birds',
+ header: 'Sisa Ayam (Ekor)',
+ accessorKey: 'remaining_chicken_birds',
+ cell: (props) => {
+ const value = props.row.original.remaining_chicken_birds;
+ return {formatNumber(value)}
;
+ },
+ footer: () => (
+
+ {formatNumber(totals.total_remaining_chicken_birds)}
+
+ ),
+ },
+ {
+ id: 'remaining_chicken_weight_kg',
+ header: 'Sisa Ayam (KG)',
+ accessorKey: 'remaining_chicken_weight_kg',
+ cell: (props) => {
+ const value = props.row.original.remaining_chicken_weight_kg;
+ return {formatNumber(value)}
;
+ },
+ footer: () => (
+
+ {formatNumber(totals.total_remaining_chicken_weight_kg)}
+
+ ),
+ },
+ {
+ id: 'avg_weight_kg',
+ header: 'Rata-Rata Bobot (KG)',
+ accessorKey: 'avg_weight_kg',
+ cell: (props) => {
+ const value = props.row.original.avg_weight_kg;
+ return {formatNumber(value)}
;
+ },
+ },
+ {
+ id: 'feed_suppliers',
+ header: 'Feed (Supplier)',
+ accessorKey: 'feed_suppliers',
+ cell: (props) => {
+ const suppliers = props.row.original.feed_suppliers;
+ return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-';
+ },
+ },
+ {
+ id: 'doc_suppliers',
+ header: 'DOC (Supplier)',
+ accessorKey: 'doc_suppliers',
+ cell: (props) => {
+ const suppliers = props.row.original.doc_suppliers;
+ return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-';
+ },
+ },
+ {
+ id: 'average_doc_price_rp',
+ header: 'Rata-Rata Harga DOC (RP)',
+ accessorKey: 'average_doc_price_rp',
+ cell: (props) => {
+ const value = props.row.original.average_doc_price_rp;
+ return {formatCurrency(value)}
;
+ },
+ },
+ {
+ id: 'hpp_rp',
+ header: 'HPP (RP)',
+ accessorKey: 'hpp_rp',
+ cell: (props) => {
+ const value = props.row.original.hpp_rp;
+ return {formatCurrency(value)}
;
+ },
+ },
+ {
+ id: 'remaining_value_rp',
+ header: 'Nilai Nominal Sisa Ayam (RP)',
+ accessorKey: 'remaining_value_rp',
+ cell: (props) => {
+ const value = props.row.original.remaining_value_rp;
+ return {formatCurrency(value)}
;
+ },
+ footer: () => (
+
+ {formatCurrency(totals.total_remaining_value_rp)}
+
+ ),
+ },
+ ];
+ return tableColumns;
+ };
+
+ return (
+
+
HPP Harian Kandang (${period})`
+ : 'Laporan > HPP Harian Kandang'
+ }
+ className={{ wrapper: 'w-full', body: 'p-1!' }}
+ >
+
+
+
+ Export}
+ align='end'
+ >
+
+
+
+
+
+
+ option.value === Number(tableFilterState.area_id)
+ ) || null
+ : null
+ }
+ onChange={areaChangeHandler}
+ isLoading={isLoadingAreas}
+ isClearable
+ />
+
+ option.value === Number(tableFilterState.location_id)
+ ) || null
+ : null
+ }
+ onChange={locationChangeHandler}
+ isLoading={isLoadingLocations}
+ isClearable
+ />
+
+ option.value === Number(tableFilterState.kandang_id)
+ ) || null
+ : null
+ }
+ onChange={kandangChangeHandler}
+ isLoading={isLoadingKandangs}
+ isClearable
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isSubmitted ? (
+
+ Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
+
+ ) : isLoading ? (
+
+
+
+ ) : data.length === 0 ? (
+
+ Tidak ada data yang dapat ditampilkan...
+
+ ) : (
+
+ 0}
+ className={{
+ containerClassName: 'w-full',
+ tableWrapperClassName: 'overflow-x-auto mt-4',
+ tableClassName: 'w-full table-auto text-sm',
+ headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
+ headerColumnClassName:
+ 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200',
+ bodyRowClassName:
+ 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200',
+ bodyColumnClassName:
+ 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
+ tableFooterClassName:
+ 'bg-gray-100 font-semibold border border-gray-200',
+ footerRowClassName: 'border-t-2 border-gray-300',
+ footerColumnClassName:
+ 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
+ paginationClassName: 'hidden',
+ }}
+ />
+
+ )}
+
+ {meta && data.length > 0 && (
+
+ )}
+
+ );
};
+
+export default HppPerKandangTab;