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 NumberInput from '@/components/input/NumberInput'; 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, Row, flexRender } from '@tanstack/react-table'; import { formatCurrency, formatNumber } from '@/lib/helper'; import { HppPerKandangReport, HppPerKandangRow, HppPerKandangPerWeightRange, } from '@/types/api/report/hpp-per-kandang'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import Button from '@/components/Button'; import Dropdown from '@/components/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'; interface Totals { total_hpp_rp: number; total_average_doc_price_rp: number; } const HppPerKandangTab = () => { // ===== STATE MANAGEMENT ===== const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { area_id: [] as string[], location_id: [] as string[], kandang_id: [] as string[], 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 showUnrecordedOptions: OptionType[] = [ { value: 'false', label: 'Sembunyikan' }, { value: 'true', label: 'Tampilkan' }, ]; const areaChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'area_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const locationChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'location_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const kandangChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const arr = Array.isArray(val) ? val : val ? [val] : []; updateFilter( 'kandang_id', arr.map((v) => String((v as OptionType).value)) ); setIsSubmitted(false); }, [updateFilter] ); const weightMinChangeHandler = useCallback< ChangeEventHandler >( (e) => { const val = e.target.value; updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); setIsSubmitted(false); }, [updateFilter] ); const weightMaxChangeHandler = useCallback< ChangeEventHandler >( (e) => { const val = e.target.value; updateFilter('weight_max', val ? String(parseFloat(val) || 0) : ''); setIsSubmitted(false); }, [updateFilter] ); const periodChangeHandler = useCallback>( (e) => { const val = e.target.value; updateFilter('period', val || ''); setIsSubmitted(false); }, [updateFilter] ); const showUnrecordedChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { const newVal = val as OptionType; updateFilter('show_unrecorded', newVal?.value === 'true'); 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); }, [tableFilterState.period]); // ===== DATA FETCHING ===== const { data: hppPerKandang, isLoading } = useSWR( isSubmitted ? () => { const params = { area_id: tableFilterState.area_id.length > 0 ? tableFilterState.area_id.join(',') : undefined, location_id: tableFilterState.location_id.length > 0 ? tableFilterState.location_id.join(',') : undefined, kandang_id: tableFilterState.kandang_id.length > 0 ? tableFilterState.kandang_id.join(',') : undefined, weight_min: tableFilterState.weight_min || undefined, weight_max: tableFilterState.weight_max || undefined, period: tableFilterState.period || undefined, sort_by: tableFilterState.sort_by || undefined, show_unrecorded: tableFilterState.show_unrecorded, }; 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 ) ); const data: HppPerKandangReport['rows'] = useMemo( () => isResponseSuccess(hppPerKandang) ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] : [], [hppPerKandang] ); const summary = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary ? hppPerKandang.data.summary : undefined; const summaryTotal = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total ? hppPerKandang.data.summary.total : undefined; const perWeightRangeSummary = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.per_weight_range ? hppPerKandang.data.summary.per_weight_range : []; const period = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period ? hppPerKandang.data.period : undefined; // ===== EXPORT DATA FETCHER ===== const hppPerKandangExport = useCallback(async (): Promise => { const params = { area_id: tableFilterState.area_id.length > 0 ? tableFilterState.area_id.join(',') : undefined, location_id: tableFilterState.location_id.length > 0 ? tableFilterState.location_id.join(',') : undefined, kandang_id: tableFilterState.kandang_id.length > 0 ? tableFilterState.kandang_id.join(',') : undefined, weight_min: tableFilterState.weight_min || undefined, weight_max: tableFilterState.weight_max || undefined, period: tableFilterState.period || undefined, sort_by: tableFilterState.sort_by || undefined, show_unrecorded: tableFilterState.show_unrecorded, limit: 10000, page: 1, }; const response = await 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 ); return isResponseSuccess(response) ? response.data : null; }, [tableFilterState]); // ===== TABLE COLUMNS DEFINITION ===== const totals: Totals = useMemo(() => { if (summaryTotal && perWeightRangeSummary.length > 0) { const totalAverageDocPrice = perWeightRangeSummary.reduce( (acc: number, item: HppPerKandangPerWeightRange) => acc + (item.average_doc_price_rp || 0), 0 ) / perWeightRangeSummary.length; return { total_hpp_rp: perWeightRangeSummary.reduce( (acc: number, item: HppPerKandangPerWeightRange) => acc + (item.hpp_rp || 0), 0 ), total_average_doc_price_rp: totalAverageDocPrice, }; } if (data.length > 0) { return { total_hpp_rp: data.reduce( (acc: number, item: HppPerKandangRow) => acc + (item.hpp_rp || 0), 0 ), total_average_doc_price_rp: data.reduce( (acc: number, item: HppPerKandangRow) => acc + (item.average_doc_price_rp || 0), 0 ) / data.length, }; } return { total_hpp_rp: 0, total_average_doc_price_rp: 0, }; }, [summaryTotal, perWeightRangeSummary, data]); const allFeedSuppliers = useMemo(() => { const suppliers = new Set(); data.forEach((item: HppPerKandangRow) => { item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => { suppliers.add(s.alias || s.name); }); }); return Array.from(suppliers).join(' | '); }, [data]); const allDocSuppliers = useMemo(() => { const suppliers = new Set(); data.forEach((item: HppPerKandangRow) => { item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => { suppliers.add(s.alias || s.name); }); }); return Array.from(suppliers).join(' | '); }, [data]); // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { const allDataForExport = await hppPerKandangExport(); if ( !allDataForExport || !allDataForExport?.rows || allDataForExport.rows.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } const allExportData = allDataForExport.rows as HppPerKandangReport['rows']; const summary = allDataForExport.summary; const excelData: { [key: string]: string | number }[] = allExportData.map( (item: HppPerKandangRow, index: number) => ({ No: index + 1, Kandang: item.kandang?.name || '', 'Rentang Bobot': item.weight_range ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` : '', 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, 'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0, 'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0, 'Produksi Telur (Butir)': item.egg_production_pieces || 0, 'Produksi Telur (KG)': item.egg_production_kg || 0, 'Feed (Supplier)': item.feed_suppliers ?.map((s: { alias?: string; name: string }) => s.alias || s.name) .join(' | ') || '', 'DOC (Supplier)': item.doc_suppliers ?.map((s: { alias?: string; name: string }) => s.alias || s.name) .join(' | ') || '', 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, 'Nilai Nominal Telur (RP)': item.egg_value_rp || 0, 'HPP Ayam (RP)': item.hpp_rp || 0, 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, 'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0, }) ); excelData.push({ No: 'TOTAL', Kandang: 'ALL', 'Rentang Bobot': '-', 'Rata-Rata Bobot (KG)': summary?.average_weight_kg || 0, 'Sisa Ayam (Ekor)': summary?.total_remaining_chicken_birds || 0, 'Sisa Ayam (KG)': summary?.total_remaining_chicken_weight_kg || 0, 'Produksi Telur (Butir)': summary?.total_egg_production_pieces || 0, 'Produksi Telur (KG)': summary?.total_egg_production_kg || 0, 'Feed (Supplier)': allFeedSuppliers, 'DOC (Supplier)': allDocSuppliers, 'Rata-Rata Harga DOC (RP)': totals?.total_average_doc_price_rp || 0, 'Nilai Nominal Telur (RP)': summary?.total_egg_value_rp || 0, 'HPP Ayam (RP)': totals?.total_hpp_rp || 0, 'HPP Telur (RP/KG)': summary?.average_egg_hpp_rp_per_kg || 0, 'Nilai Nominal Sisa Ayam (RP)': summary?.total_remaining_value_rp || 0, }); const worksheet = XLSX.utils.json_to_sheet(excelData); const colWidths = [ { wch: 5 }, // No { wch: 30 }, // Kandang { wch: 15 }, // Rentang Bobot { wch: 18 }, // Rata-Rata Bobot (KG) { wch: 15 }, // Sisa Ayam (Ekor) { wch: 15 }, // Sisa Ayam (KG) { wch: 18 }, // Produksi Telur (Butir) { wch: 18 }, // Produksi Telur (KG) { wch: 20 }, // Feed (Supplier) { wch: 20 }, // DOC (Supplier) { wch: 20 }, // Rata-Rata Harga DOC (RP) { wch: 20 }, // Nilai Nominal Telur (RP) { wch: 15 }, // HPP Ayam (RP) { wch: 18 }, // HPP Telur (RP/KG) { 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 filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); } finally { setIsExcelExportLoading(false); } }, [ hppPerKandangExport, tableFilterState, areaOptions, locationOptions, kandangOptions, ]); const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); try { const allDataForExport = await hppPerKandangExport(); if ( !allDataForExport || !allDataForExport?.rows || allDataForExport.rows.length === 0 ) { toast.error('Tidak ada data untuk diekspor.'); return; } const areaName = tableFilterState.area_id.length > 0 ? tableFilterState.area_id .map( (id) => areaOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Area' : 'Semua Area'; const locationName = tableFilterState.location_id.length > 0 ? tableFilterState.location_id .map( (id) => locationOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Lokasi' : 'Semua Lokasi'; const kandangName = tableFilterState.kandang_id.length > 0 ? tableFilterState.kandang_id .map( (id) => kandangOptions.find((opt) => opt.value === Number(id))?.label ) .filter(Boolean) .join(', ') || 'Semua Kandang' : 'Semua Kandang'; await generateHppPerKandangPDF(allDataForExport, { 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.'); } finally { setIsPdfExportLoading(false); } }, [ hppPerKandangExport, tableFilterState, areaOptions, locationOptions, kandangOptions, ]); 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 || '-'; }, footer: () =>
ALL
, }, { 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)}` : '-'; }, footer: () =>
-
, }, { 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)}
; }, footer: () => (
{formatNumber(summary?.total?.average_weight_kg || 0)}
), }, { 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(summary?.total?.total_remaining_chicken_birds || 0)}
), }, { 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( summary?.total?.total_remaining_chicken_weight_kg || 0 )}
), }, { id: 'egg_production_pieces', header: 'Produksi Telur (Butir)', accessorKey: 'egg_production_pieces', cell: (props) => { const value = props.row.original.egg_production_pieces; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber(summary?.total?.total_egg_production_pieces || 0)}
), }, { id: 'egg_production_kg', header: 'Produksi Telur (KG)', accessorKey: 'egg_production_kg', cell: (props) => { const value = props.row.original.egg_production_kg; return
{formatNumber(value)}
; }, footer: () => (
{formatNumber( summary?.total?.total_remaining_chicken_weight_kg || 0 )}
), }, { id: 'feed_suppliers', header: 'Feed (Supplier)', accessorKey: 'feed_suppliers', cell: (props) => { const suppliers = props.row.original.feed_suppliers; return ( suppliers ?.map((s: { alias?: string; name: string }) => s.alias || s.name) .join(' | ') || '-' ); }, footer: () => (
{allFeedSuppliers || '-'}
), }, { id: 'doc_suppliers', header: 'DOC (Supplier)', accessorKey: 'doc_suppliers', cell: (props) => { const suppliers = props.row.original.doc_suppliers; return ( suppliers ?.map((s: { alias?: string; name: string }) => s.alias || s.name) .join(' | ') || '-' ); }, footer: () => (
{allDocSuppliers || '-'}
), }, { 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)}
; }, footer: () => (
{formatCurrency(totals?.total_average_doc_price_rp || 0)}
), }, { id: 'egg_value_rp', header: 'Nilai Nominal Telur (RP)', accessorKey: 'egg_value_rp', cell: (props) => { const value = props.row.original.egg_value_rp; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary?.total?.total_egg_value_rp || 0)}
), }, { id: 'hpp_rp', header: 'HPP Ayam (RP)', accessorKey: 'hpp_rp', cell: (props) => { const value = props.row.original.hpp_rp; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(totals?.total_hpp_rp || 0)}
), }, { id: 'egg_hpp_rp_per_kg', header: 'HPP Telur (RP/KG)', accessorKey: 'egg_hpp_rp_per_kg', cell: (props) => { const value = props.row.original.egg_hpp_rp_per_kg; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency(summary?.total?.average_egg_hpp_rp_per_kg || 0)}
), }, { 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(summary?.total?.total_remaining_value_rp || 0)}
), }, ]; return tableColumns; }; // ===== CUSTOM ROW RENDERER ===== const renderCustomRow = useCallback( (row: Row) => { if (row.index === data.length - 1) { const defaultRow = ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ); const customRows = [ Rekapitulasi per rentang bobot , ]; if (perWeightRangeSummary.length > 0) { perWeightRangeSummary.forEach( (item: HppPerKandangPerWeightRange, index = 0) => { customRows.push( {index + 1} ALL {item.label} {formatNumber(item.avg_weight_kg)} {formatNumber(item.remaining_chicken_birds)} {formatNumber(item.remaining_chicken_weight_kg)} - - {item.feed_suppliers ?.map((s) => s.alias || s.name) .join(' | ') || '-'} {item.doc_suppliers ?.map((s) => s.alias || s.name) .join(' | ') || '-'} {formatCurrency(item.average_doc_price_rp)} - {formatCurrency(item.hpp_rp)} - {formatCurrency(item.remaining_value_rp)} ); } ); } return [defaultRow, ...customRows]; } return null; }, [data, perWeightRangeSummary] ); return (
HPP Harian Kandang (${period})` : 'Laporan > HPP Harian Kandang' } className={{ wrapper: 'w-full', body: 'p-1!' }} >
Export } align='end' >
(tableFilterState.area_id || []) .map(String) .includes(String(opt.value)) )} onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable /> (tableFilterState.location_id || []) .map(String) .includes(String(opt.value)) )} onChange={locationChangeHandler} isLoading={isLoadingLocations} isClearable /> (tableFilterState.kandang_id || []) .map(String) .includes(String(opt.value)) )} onChange={kandangChangeHandler} isLoading={isLoadingKandangs} isClearable />
opt.value === 'true') || null : showUnrecordedOptions.find((opt) => opt.value === 'false') || null } onChange={showUnrecordedChangeHandler} />
{!isSubmitted ? (
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
) : isLoading ? (
) : data.length === 0 ? (
Tidak ada data yang dapat ditampilkan...
) : ( 0} renderCustomRow={renderCustomRow} className={{ containerClassName: 'w-full mt-6', 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', }} /> )} ); }; export default HppPerKandangTab;