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 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'; 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); // ===== 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); }, [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, }; 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 period = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period ? hppPerKandang.data.period : 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, }; 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 ) ); 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, ]); 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 { 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 allFeedSuppliers = useMemo(() => { const suppliers = new Set(); data.forEach((item) => { item.feed_suppliers?.forEach((s) => { suppliers.add(s.alias || s.name); }); }); return Array.from(suppliers).join(' | '); }, [data]); const allDocSuppliers = useMemo(() => { const suppliers = new Set(); data.forEach((item) => { item.doc_suppliers?.forEach((s) => { suppliers.add(s.alias || s.name); }); }); return Array.from(suppliers).join(' | '); }, [data]); 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: '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)}
; }, footer: () => (
{formatNumber(summary?.average_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) => 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) => 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( summary?.total_remaining_value_rp && summary?.total_remaining_chicken_birds ? summary.total_remaining_value_rp / summary.total_remaining_chicken_birds : 0 )}
), }, { id: 'hpp_rp', header: 'HPP (RP)', accessorKey: 'hpp_rp', cell: (props) => { const value = props.row.original.hpp_rp; return
{formatCurrency(value)}
; }, footer: () => (
{formatCurrency( summary?.total_remaining_value_rp && summary?.total_remaining_chicken_birds ? summary.total_remaining_value_rp / summary.total_remaining_chicken_birds : 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(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 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;