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' + > + + + { + alert('Fitur belum tersedia'); + }} + /> + + +
+ +
+ + 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;