From 0f7f4e891c3624222ac12cf6855e02e210b829be Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 09:50:36 +0700 Subject: [PATCH 01/50] feat(FE-357): Add marketing sales report API and types --- src/config/constant.ts | 11 +++++ src/services/api/report/marketing-sale.ts | 50 +++++++++++++++++++++++ src/types/api/report/hpp-per-kandang.d.ts | 41 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/services/api/report/marketing-sale.ts create mode 100644 src/types/api/report/hpp-per-kandang.d.ts diff --git a/src/config/constant.ts b/src/config/constant.ts index 96fc8401..6d25f56a 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -45,6 +45,17 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/closing', icon: 'heroicons-outline:presentation-chart-bar', }, + { + text: 'Laporan', + link: '/report', + icon: 'mdi:chart-box-outline', + submenu: [ + { + text: 'Penjualan', + link: '/report/marketing', + }, + ], + }, { text: 'Persediaan', link: '/inventory', diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts new file mode 100644 index 00000000..c18871e3 --- /dev/null +++ b/src/services/api/report/marketing-sale.ts @@ -0,0 +1,50 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { HppPerkandang } from '@/types/api/report/hpp-per-kandang'; + +export class MarketingSaleReportService extends BaseApiService< + HppPerkandang, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getHppPerKandangReport( + area_id?: number, + location_id?: number, + kandang_id?: number, + weight_min?: number, + weight_max?: number, + period?: string, + sort_by?: string, + show_unrecorded?: boolean, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest>( + `hpp-per-kandang`, + { + method: 'GET', + params: { + area_id: area_id, + location_id: location_id, + kandang_id: kandang_id, + weight_min: weight_min, + weight_max: weight_max, + period: period, + sort_by: sort_by, + show_unrecorded: show_unrecorded, + page: page, + limit: limit, + }, + } + ); + } +} + +// TODO: REPLACE WITH PRODUCTION URL +export const SaleReportApi = new MarketingSaleReportService( + 'http://localhost:4010/api/reports/marketings/hpp-per-kandang' +); diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts new file mode 100644 index 00000000..fab7b5c6 --- /dev/null +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -0,0 +1,41 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Supplier } from '@/types/api/supplier/supplier'; +import { Kandang } from '@/types/api/master-data/kandang'; + +export type BaseHppPerKandangSummary = { + total_remaining_chicken_birds: number; + total_remaining_chicken_weight_kg: number; + average_weight_kg: number; + total_remaining_value_rp: number; + total_egg_production_pieces: number; + total_egg_production_kg: number; + average_egg_hpp_rp_per_kg: number; + total_egg_value_rp: number; +}; + +export type BaseHppPerkandang = { + id: number; + kandang: Kandang; + weight_range: { + weight_min: number; + weight_max: number; + }; + remaining_chicken_birds: number; + remaining_chicken_weight_kg: number; + avg_weight_kg: number; + egg_production_pieces: number; + egg_production_kg: number; + egg_hpp_rp_per_kg: number; + egg_value_rp: number; + feed_suppliers: Supplier; + doc_suppliers: Supplier; + average_doc_price_rp: number; + hpp_rp: number; + remaining_value_rp: number; +}; + +export type HppPerkandang = BaseMetadata & { + periods: string; + rows: SalesReport[]; + summary: BaseSalesReportSummary; +}; From 43afc5781c0d2765413fe6bd4b76fbee2b7c81c4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 09:50:55 +0700 Subject: [PATCH 02/50] feat(FE-355): Add sale report tabs and marketing layout --- src/app/report/marketing/layout.tsx | 11 ++++++ src/app/report/marketing/page.tsx | 7 ++++ .../pages/report/sale/SaleReportTabs.tsx | 37 +++++++++++++++++++ .../report/sale/tab/HppPerKandangTab.tsx | 3 ++ 4 files changed, 58 insertions(+) create mode 100644 src/app/report/marketing/layout.tsx create mode 100644 src/app/report/marketing/page.tsx create mode 100644 src/components/pages/report/sale/SaleReportTabs.tsx create mode 100644 src/components/pages/report/sale/tab/HppPerKandangTab.tsx diff --git a/src/app/report/marketing/layout.tsx b/src/app/report/marketing/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/marketing/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/report/marketing/page.tsx b/src/app/report/marketing/page.tsx new file mode 100644 index 00000000..a330bc1f --- /dev/null +++ b/src/app/report/marketing/page.tsx @@ -0,0 +1,7 @@ +import SaleReportTabs from '@/components/pages/report/sale/SaleReportTabs'; + +const SaleReport = () => { + return ; +}; + +export default SaleReport; diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx new file mode 100644 index 00000000..5c5ee473 --- /dev/null +++ b/src/components/pages/report/sale/SaleReportTabs.tsx @@ -0,0 +1,37 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import { HppPerKandangTab } from '@/components/pages/report/sale/tab/HppPerKandangTab'; + +const SaleReportTabs = () => { + const tabs = [ + { + id: '1', + label: 'Penjualan Harian', + content: 'Penjualan Harian Tab', + }, + { + id: '2', + label: 'Transaksi Penjualan DO', + content: 'Transaksi Penjualan DO Tab', + }, + { + id: '3', + label: 'Perbandingan HPP Per Rentang BW', + content: 'Perbandingan HPP Per Rentang BW Tab', + }, + { + id: '4', + label: 'HPP Harian Kandang', + content: , + }, + ]; + + return ( +
+ +
+ ); +}; + +export default SaleReportTabs; diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx new file mode 100644 index 00000000..60320597 --- /dev/null +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -0,0 +1,3 @@ +export const HppPerKandangTab = () => { + return
HPP Per Kandang Tab
; +}; From 12e6d607456b1e23415760284063be08ec935f4f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 10:37:05 +0700 Subject: [PATCH 03/50] feat(FE-357): Rename HppPerkandang types and update API path --- src/services/api/report/marketing-sale.ts | 10 +++++----- src/types/api/report/hpp-per-kandang.d.ts | 19 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index c18871e3..e15d2a8b 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -1,9 +1,9 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; -import { HppPerkandang } from '@/types/api/report/hpp-per-kandang'; +import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; export class MarketingSaleReportService extends BaseApiService< - HppPerkandang, + HppPerKandangReport, unknown, unknown > { @@ -22,8 +22,8 @@ export class MarketingSaleReportService extends BaseApiService< show_unrecorded?: boolean, page?: number, limit?: number - ): Promise | undefined> { - return await this.customRequest>( + ): Promise | undefined> { + return await this.customRequest>( `hpp-per-kandang`, { method: 'GET', @@ -46,5 +46,5 @@ export class MarketingSaleReportService extends BaseApiService< // TODO: REPLACE WITH PRODUCTION URL export const SaleReportApi = new MarketingSaleReportService( - 'http://localhost:4010/api/reports/marketings/hpp-per-kandang' + 'http://localhost:4010/api/reports/marketings' ); diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts index fab7b5c6..2b4522a0 100644 --- a/src/types/api/report/hpp-per-kandang.d.ts +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -1,8 +1,7 @@ -import { BaseMetadata } from '@/types/api/api-general'; -import { Supplier } from '@/types/api/supplier/supplier'; +import { Supplier } from '@/types/api/master-data/supplier'; import { Kandang } from '@/types/api/master-data/kandang'; -export type BaseHppPerKandangSummary = { +export type HppPerKandangSummary = { total_remaining_chicken_birds: number; total_remaining_chicken_weight_kg: number; average_weight_kg: number; @@ -13,7 +12,7 @@ export type BaseHppPerKandangSummary = { total_egg_value_rp: number; }; -export type BaseHppPerkandang = { +export type HppPerKandangRow = { id: number; kandang: Kandang; weight_range: { @@ -27,15 +26,15 @@ export type BaseHppPerkandang = { egg_production_kg: number; egg_hpp_rp_per_kg: number; egg_value_rp: number; - feed_suppliers: Supplier; - doc_suppliers: Supplier; + feed_suppliers: Supplier[]; + doc_suppliers: Supplier[]; average_doc_price_rp: number; hpp_rp: number; remaining_value_rp: number; }; -export type HppPerkandang = BaseMetadata & { - periods: string; - rows: SalesReport[]; - summary: BaseSalesReportSummary; +export type HppPerKandangReport = { + period: string; + rows: HppPerKandangRow[]; + summary: HppPerKandangSummary; }; From 3d91c1287435328e976f315cc625ba28bcc6dfbe Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 10:38:56 +0700 Subject: [PATCH 04/50] feat(FE-355,356,357): Implement HPP Per Kandang report tab --- .../pages/report/sale/SaleReportTabs.tsx | 2 +- .../report/sale/tab/HppPerKandangTab.tsx | 745 +++++++++++++++++- 2 files changed, 744 insertions(+), 3 deletions(-) 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; From 78efd587be2ffae838bb8739126d2ac2ea41f2ac Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 10:54:02 +0700 Subject: [PATCH 05/50] feat(FE-355,357): Add aggregated footers for HPP per kandang table --- .../report/sale/tab/HppPerKandangTab.tsx | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 6d72f2d8..7982ba99 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -431,6 +431,52 @@ const HppPerKandangTab = () => { }; }, [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 weightedAverageDocPrice = useMemo(() => { + if (data.length === 0) return 0; + const totalValue = data.reduce( + (sum, item) => sum + (item.hpp_rp * item.remaining_chicken_birds || 0), + 0 + ); + const totalChickens = data.reduce( + (sum, item) => sum + (item.remaining_chicken_birds || 0), + 0 + ); + return totalChickens > 0 ? totalValue / totalChickens : 0; + }, [data]); + + const weightedAverageHpp = useMemo(() => { + if (data.length === 0) return 0; + const totalValue = data.reduce( + (sum, item) => sum + (item.hpp_rp * item.remaining_chicken_birds || 0), + 0 + ); + const totalChickens = data.reduce( + (sum, item) => sum + (item.remaining_chicken_birds || 0), + 0 + ); + return totalChickens > 0 ? totalValue / totalChickens : 0; + }, [data]); + const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -447,6 +493,7 @@ const HppPerKandangTab = () => { const kandang = props.row.original.kandang; return kandang?.name || '-'; }, + footer: () =>
ALL
, }, { id: 'weight_range', @@ -458,6 +505,7 @@ const HppPerKandangTab = () => { ? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}` : '-'; }, + footer: () =>
-
, }, { id: 'remaining_chicken_birds', @@ -495,6 +543,11 @@ const HppPerKandangTab = () => { const value = props.row.original.avg_weight_kg; return
{formatNumber(value)}
; }, + footer: () => ( +
+ {formatNumber(summary?.average_weight_kg || 0)} +
+ ), }, { id: 'feed_suppliers', @@ -504,6 +557,11 @@ const HppPerKandangTab = () => { const suppliers = props.row.original.feed_suppliers; return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-'; }, + footer: () => ( +
+ {allFeedSuppliers || '-'} +
+ ), }, { id: 'doc_suppliers', @@ -513,6 +571,11 @@ const HppPerKandangTab = () => { const suppliers = props.row.original.doc_suppliers; return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-'; }, + footer: () => ( +
+ {allDocSuppliers || '-'} +
+ ), }, { id: 'average_doc_price_rp', @@ -522,6 +585,11 @@ const HppPerKandangTab = () => { const value = props.row.original.average_doc_price_rp; return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(weightedAverageDocPrice)} +
+ ), }, { id: 'hpp_rp', @@ -531,6 +599,11 @@ const HppPerKandangTab = () => { const value = props.row.original.hpp_rp; return
{formatCurrency(value)}
; }, + footer: () => ( +
+ {formatCurrency(weightedAverageHpp)} +
+ ), }, { id: 'remaining_value_rp', From 730b7903a76bca6511205a152148444bd5a02110 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:01:01 +0700 Subject: [PATCH 06/50] feat(FE-355): Remove pagination from HppPerKandangTab --- .../report/sale/tab/HppPerKandangTab.tsx | 113 ++++-------------- 1 file changed, 25 insertions(+), 88 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7982ba99..209011c5 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -37,10 +37,6 @@ 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); @@ -162,7 +158,6 @@ const HppPerKandangTab = () => { return; } setIsSubmitted(true); - setCurrentPage(1); }, [tableFilterState.period]); // ===== DATA FETCHING ===== @@ -188,8 +183,6 @@ const HppPerKandangTab = () => { 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]; @@ -204,9 +197,7 @@ const HppPerKandangTab = () => { params.weight_max, params.period, params.sort_by, - params.show_unrecorded, - params.page, - params.limit + params.show_unrecorded ) ); @@ -255,8 +246,6 @@ const HppPerKandangTab = () => { 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]; @@ -271,9 +260,7 @@ const HppPerKandangTab = () => { params.weight_max, params.period, params.sort_by, - params.show_unrecorded, - params.page, - params.limit + params.show_unrecorded ) ); @@ -399,27 +386,6 @@ const HppPerKandangTab = () => { 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 { @@ -756,60 +722,31 @@ const HppPerKandangTab = () => { 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', - }} - /> - +
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 && ( -
- -
- )} ); }; From 4cc41c016767063f7660ca5dc817b2e0b3a28faf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:11:44 +0700 Subject: [PATCH 07/50] feat(FE-355,357): Use API summary for weighted averages --- .../report/sale/tab/HppPerKandangTab.tsx | 57 +++++++------------ 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 209011c5..0481dd2a 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -19,7 +19,6 @@ 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'; @@ -210,20 +209,15 @@ const HppPerKandangTab = () => { ); const summary = - isResponseSuccess(hppPerKandang) && 'summary' in hppPerKandang + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary ? hppPerKandang.data.summary : undefined; const period = - isResponseSuccess(hppPerKandang) && 'period' in hppPerKandang + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period ? hppPerKandang.data.period : undefined; - const meta = - isResponseSuccess(hppPerKandang) && 'meta' in hppPerKandang - ? hppPerKandang.meta - : undefined; - const { data: allDataForExport } = useSWR( isSubmitted ? () => { @@ -417,32 +411,6 @@ const HppPerKandangTab = () => { return Array.from(suppliers).join(' | '); }, [data]); - const weightedAverageDocPrice = useMemo(() => { - if (data.length === 0) return 0; - const totalValue = data.reduce( - (sum, item) => sum + (item.hpp_rp * item.remaining_chicken_birds || 0), - 0 - ); - const totalChickens = data.reduce( - (sum, item) => sum + (item.remaining_chicken_birds || 0), - 0 - ); - return totalChickens > 0 ? totalValue / totalChickens : 0; - }, [data]); - - const weightedAverageHpp = useMemo(() => { - if (data.length === 0) return 0; - const totalValue = data.reduce( - (sum, item) => sum + (item.hpp_rp * item.remaining_chicken_birds || 0), - 0 - ); - const totalChickens = data.reduce( - (sum, item) => sum + (item.remaining_chicken_birds || 0), - 0 - ); - return totalChickens > 0 ? totalValue / totalChickens : 0; - }, [data]); - const getTableColumns = (): ColumnDef[] => { const tableColumns: ColumnDef[] = [ { @@ -553,7 +521,13 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(weightedAverageDocPrice)} + {formatCurrency( + summary?.total_remaining_value_rp && + summary?.total_remaining_chicken_birds + ? summary.total_remaining_value_rp / + summary.total_remaining_chicken_birds + : 0 + )}
), }, @@ -567,7 +541,13 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(weightedAverageHpp)} + {formatCurrency( + summary?.total_remaining_value_rp && + summary?.total_remaining_chicken_birds + ? summary.total_remaining_value_rp / + summary.total_remaining_chicken_birds + : 0 + )}
), }, @@ -709,6 +689,8 @@ const HppPerKandangTab = () => { /> +
+ {!isSubmitted ? (
Silakan pilih filter dan klik tombol Cari untuk menampilkan data. @@ -727,7 +709,7 @@ const HppPerKandangTab = () => { columns={getTableColumns()} renderFooter={data.length > 0} className={{ - containerClassName: 'w-full', + 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', @@ -742,7 +724,6 @@ const HppPerKandangTab = () => { footerRowClassName: 'border-t-2 border-gray-300', footerColumnClassName: 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - paginationClassName: 'hidden', }} /> )} From c70cfbd450b1c34699be49e3b429055474518acb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:42:45 +0700 Subject: [PATCH 08/50] feat(FE-355,356,357): Add PDF export for HPP per kandang report --- .../sale/export/HppPerkandangExport.tsx | 490 ++++++++++++++++++ .../report/sale/tab/HppPerKandangTab.tsx | 56 +- 2 files changed, 540 insertions(+), 6 deletions(-) create mode 100644 src/components/pages/report/sale/export/HppPerkandangExport.tsx 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 = () => { > - { - alert('Fitur belum tersedia'); - }} - /> +
From c1e075b1ff98b2a657c7ba0e5d380c1625ba09ab Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:47:12 +0700 Subject: [PATCH 09/50] refactor(FE): refactor Dropdown component API and Navbar usage --- src/components/Dropdown.tsx | 114 ++++++++++++++++++++++++++ src/components/Navbar.tsx | 11 ++- src/components/dropdown/Dropdown.tsx | 116 --------------------------- 3 files changed, 121 insertions(+), 120 deletions(-) create mode 100644 src/components/Dropdown.tsx delete mode 100644 src/components/dropdown/Dropdown.tsx diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx new file mode 100644 index 00000000..5bfa7a7d --- /dev/null +++ b/src/components/Dropdown.tsx @@ -0,0 +1,114 @@ +import React, { ReactNode, useState, useRef } from 'react'; + +import { cn } from '@/lib/helper'; + +export interface DropdownProps { + trigger: ReactNode; + children: ReactNode; + className?: { + wrapper?: string; + trigger?: string; + content?: string; + }; + align?: 'start' | 'center' | 'end'; + direction?: 'top' | 'bottom' | 'left' | 'right'; + hover?: boolean; + defaultOpen?: boolean; + open?: boolean; + close?: boolean; + controlled?: boolean; +} + +const Dropdown = ({ + trigger, + children, + className, + align, + direction, + hover, + defaultOpen = false, + open, + close, + controlled = false, +}: DropdownProps) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + const dropdownRef = useRef(null); + + const toggleDropdown = () => { + if (!controlled) { + const newState = !isOpen; + setIsOpen(newState); + } + }; + + const getWrapperClasses = () => { + const openState = controlled ? open : isOpen; + + return cn( + 'dropdown', + { + 'dropdown-start': align === 'start', + 'dropdown-center': align === 'center', + 'dropdown-end': align === 'end', + 'dropdown-top': direction === 'top', + 'dropdown-bottom': direction === 'bottom', + 'dropdown-left': direction === 'left', + 'dropdown-right': direction === 'right', + 'dropdown-hover': hover, + 'dropdown-open': openState && !close, + 'dropdown-close': close, + }, + className?.wrapper + ); + }; + + const getTriggerClasses = () => { + return cn(className?.trigger); + }; + + const getContentClasses = () => { + return cn( + 'dropdown-content z-[9999] shadow-sm bg-base-100 rounded-box', + className?.content + ); + }; + + if (controlled) { + return ( +
+ {trigger} + {open && !close && ( +
+ {children} +
+ )} +
+ ); + } + + return ( +
+
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleDropdown(); + } + }} + > + {trigger} +
+ {!close && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Dropdown; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index bee92a57..280217a0 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -7,7 +7,7 @@ import { Icon } from '@iconify/react'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; import Button from '@/components/Button'; -import Dropdown from '@/components/dropdown/Dropdown'; +import Dropdown from '@/components/Dropdown'; import { useAuth } from '@/services/hooks/useAuth'; import { AuthApi } from '@/services/api/auth'; @@ -54,7 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
@@ -62,9 +63,11 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
} - contentClassName='w-52 mt-3' + className={{ + content: 'w-52 mt-3', + }} > - + diff --git a/src/components/dropdown/Dropdown.tsx b/src/components/dropdown/Dropdown.tsx deleted file mode 100644 index 4489231d..00000000 --- a/src/components/dropdown/Dropdown.tsx +++ /dev/null @@ -1,116 +0,0 @@ -'use client'; - -import { ReactNode, useRef, useEffect, useState } from 'react'; -import { cn } from '@/lib/helper'; - -interface DropdownProps { - trigger: ReactNode; - children: ReactNode; - position?: - | 'top' - | 'bottom' - | 'left' - | 'right' - | 'top-start' - | 'top-end' - | 'bottom-start' - | 'bottom-end' - | 'left-start' - | 'left-end' - | 'right-start' - | 'right-end'; - align?: 'start' | 'center' | 'end'; - hover?: boolean; - className?: string; - contentClassName?: string; -} - -const Dropdown = ({ - trigger, - children, - position = 'bottom', - align = 'start', - hover = false, - className, - contentClassName, -}: DropdownProps) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - // Handle click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsOpen(false); - } - }; - - if (isOpen) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [isOpen]); - - // Build position classes - const getPositionClasses = () => { - const classes: string[] = []; - - // Handle combined positions like 'top-start' - if (position.includes('-')) { - const [pos, al] = position.split('-'); - classes.push(`dropdown-${pos}`); - classes.push(`dropdown-${al}`); - } else { - classes.push(`dropdown-${position}`); - if (align !== 'start') { - classes.push(`dropdown-${align}`); - } - } - - return classes.join(' '); - }; - - const handleToggle = (e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - // alert('clicked'); - setIsOpen(!isOpen); - }; - - return ( -
- {/* Trigger Button */} -
- {trigger} -
- - {/* Dropdown Content - Only render when open */} - {isOpen && ( -
setIsOpen(false)} // Close on item click - > - {children} -
- )} -
- ); -}; - -export default Dropdown; From 62d4d7b7dbc5e759e422e399fc54da71053ba7f1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:49:41 +0700 Subject: [PATCH 10/50] refactor(FE): Fix Dropdown import path --- src/components/pages/report/sale/tab/HppPerKandangTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index d90040c6..9dd1dcf3 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -20,7 +20,7 @@ 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 Dropdown from '@/components/Dropdown'; import MenuItem from '@/components/menu/MenuItem'; import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; From a8c3b1a66f19cf4d317248828f749fb861612acd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 11:56:42 +0700 Subject: [PATCH 11/50] feat(FE-356): Include HPP and supplier aggregates in export --- .../report/sale/tab/HppPerKandangTab.tsx | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 9dd1dcf3..1f6bdc7f 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -285,14 +285,44 @@ const HppPerKandangTab = () => { (item.remaining_chicken_weight_kg || 0), total_remaining_value_rp: acc.total_remaining_value_rp + (item.remaining_value_rp || 0), + total_hpp_rp: acc.total_hpp_rp + (item.hpp_rp || 0), + average_doc_price_rp: + acc.average_doc_price_rp + (item.average_doc_price_rp || 0), + all_feed_suppliers: new Set([ + ...acc.all_feed_suppliers, + ...(item.feed_suppliers?.map((s) => s.alias || s.name) || []), + ]), + all_doc_suppliers: new Set([ + ...acc.all_doc_suppliers, + ...(item.doc_suppliers?.map((s) => s.alias || s.name) || []), + ]), }), { total_remaining_chicken_birds: 0, total_remaining_chicken_weight_kg: 0, total_remaining_value_rp: 0, + total_hpp_rp: 0, + average_doc_price_rp: 0, + all_feed_suppliers: new Set(), + all_doc_suppliers: new Set(), } ); + // Calculate averages + const avgWeight = + totals.total_remaining_chicken_birds > 0 + ? totals.total_remaining_chicken_weight_kg / + totals.total_remaining_chicken_birds + : 0; + const avgDocPrice = + allExportData.length > 0 + ? totals.average_doc_price_rp / allExportData.length + : 0; + const avgHpp = + totals.total_remaining_chicken_birds > 0 + ? totals.total_hpp_rp / totals.total_remaining_chicken_birds + : 0; + const excelData: { [key: string]: string | number }[] = allExportData.map( (item, index) => ({ No: index + 1, @@ -320,11 +350,11 @@ const HppPerKandangTab = () => { '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)': '', + 'Rata-Rata Bobot (KG)': avgWeight, + 'Feed (Supplier)': Array.from(totals.all_feed_suppliers).join(' | '), + 'DOC (Supplier)': Array.from(totals.all_doc_suppliers).join(' | '), + 'Rata-Rata Harga DOC (RP)': avgDocPrice, + 'HPP (RP)': avgHpp, 'Nilai Nominal Sisa Ayam (RP)': totals.total_remaining_value_rp, }); From 530ef4982d917252711da85f335611b9666aeffb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 13:04:26 +0700 Subject: [PATCH 12/50] refactor(FE-355,357): Use summary fields for egg HPP and total value --- .../pages/report/sale/tab/HppPerKandangTab.tsx | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 1f6bdc7f..3a948a0f 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -600,13 +600,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency( - summary?.total_remaining_value_rp && - summary?.total_remaining_chicken_birds - ? summary.total_remaining_value_rp / - summary.total_remaining_chicken_birds - : 0 - )} + {formatCurrency(summary?.average_egg_hpp_rp_per_kg || 0)}
), }, @@ -620,13 +614,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency( - summary?.total_remaining_value_rp && - summary?.total_remaining_chicken_birds - ? summary.total_remaining_value_rp / - summary.total_remaining_chicken_birds - : 0 - )} + {formatCurrency(summary?.total_egg_value_rp || 0)}
), }, From e5154383120b16d15d7e9556cba04ac1454dd183 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 13:33:51 +0700 Subject: [PATCH 13/50] refactor(FE-355): Use NumberInput and SelectInput for filters --- .../report/sale/tab/HppPerKandangTab.tsx | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 3a948a0f..e50667fd 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -7,8 +7,7 @@ import SelectInput, { OptionType, } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; -import TextInput from '@/components/input/TextInput'; -import CheckboxInput from '@/components/input/CheckboxInput'; +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'; @@ -34,9 +33,6 @@ interface Totals { } const HppPerKandangTab = () => { - // ===== STATE MANAGEMENT ===== - const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); - // ===== SUBMISSION STATE ===== const [isSubmitted, setIsSubmitted] = useState(false); @@ -71,6 +67,11 @@ const HppPerKandangTab = () => { 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 newVal = val as OptionType; @@ -103,7 +104,7 @@ const HppPerKandangTab = () => { >( (e) => { const val = e.target.value; - updateFilter('weight_min', val || ''); + updateFilter('weight_min', val ? String(parseInt(val, 10)) : ''); setIsSubmitted(false); }, [updateFilter] @@ -114,7 +115,7 @@ const HppPerKandangTab = () => { >( (e) => { const val = e.target.value; - updateFilter('weight_max', val || ''); + updateFilter('weight_max', val ? String(parseInt(val, 10)) : ''); setIsSubmitted(false); }, [updateFilter] @@ -129,12 +130,10 @@ const HppPerKandangTab = () => { [updateFilter] ); - const showUnrecordedChangeHandler = useCallback< - ChangeEventHandler - >( - (e) => { - const checked = e.target.checked; - updateFilter('show_unrecorded', checked); + const showUnrecordedChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateFilter('show_unrecorded', newVal?.value === 'true'); setIsSubmitted(false); }, [updateFilter] @@ -308,7 +307,6 @@ const HppPerKandangTab = () => { } ); - // Calculate averages const avgWeight = totals.total_remaining_chicken_birds > 0 ? totals.total_remaining_chicken_weight_kg / @@ -716,22 +714,22 @@ const HppPerKandangTab = () => {
- - +
+ + +
{ onChange={periodChangeHandler} required /> -
- -
- opt.value === 'true') || + null + : showUnrecordedOptions.find((opt) => opt.value === 'false') || + null + } onChange={showUnrecordedChangeHandler} />
From 9365320b03ef2e2c5693e425b4b717c253b7968f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 13:45:00 +0700 Subject: [PATCH 14/50] feat(FE-357): Add loading state for export --- .../pages/report/sale/tab/HppPerKandangTab.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index e50667fd..620f41d6 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -33,6 +33,11 @@ interface Totals { } 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); @@ -273,6 +278,7 @@ const HppPerKandangTab = () => { return; } + setIsExcelExportLoading(true); try { const totals = allExportData.reduce( (acc, item) => ({ @@ -400,6 +406,8 @@ const HppPerKandangTab = () => { toast.success('Excel berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); } }, [ allExportData, @@ -415,6 +423,7 @@ const HppPerKandangTab = () => { return; } + setIsPdfExportLoading(true); try { const areaName = tableFilterState.area_id ? areaOptions.find( @@ -448,6 +457,8 @@ const HppPerKandangTab = () => { toast.success('PDF berhasil dibuat dan diunduh.'); } catch { toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); } }, [ hppPerKandang, @@ -652,7 +663,11 @@ const HppPerKandangTab = () => { Reset Export} + trigger={ + + } align='end' > From e1b562c17599562e970fdce2dcb6e9423d97238e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 14:05:29 +0700 Subject: [PATCH 15/50] refactor(FE-357): Refactor export data fetching and handlers --- .../report/sale/tab/HppPerKandangTab.tsx | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 620f41d6..38ac1649 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -165,7 +165,7 @@ const HppPerKandangTab = () => { }, [tableFilterState.period]); // ===== DATA FETCHING ===== - const { data: hppPerKandang, isLoading } = useSWR( + const { data: hppPerKandangResponse, isLoading } = useSWR( isSubmitted ? () => { const params = { @@ -207,51 +207,52 @@ const HppPerKandangTab = () => { const data: HppPerKandangReport['rows'] = useMemo( () => - isResponseSuccess(hppPerKandang) - ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] + isResponseSuccess(hppPerKandangResponse) + ? (hppPerKandangResponse?.data?.rows as HppPerKandangReport['rows']) || + [] : [], - [hppPerKandang] + [hppPerKandangResponse] ); const summary = - isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary - ? hppPerKandang.data.summary + isResponseSuccess(hppPerKandangResponse) && + hppPerKandangResponse?.data?.summary + ? hppPerKandangResponse.data.summary : undefined; const period = - isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period - ? hppPerKandang.data.period + isResponseSuccess(hppPerKandangResponse) && + hppPerKandangResponse?.data?.period + ? hppPerKandangResponse.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, - }; + // ===== EXPORT DATA FETCHER ===== + const fetchAllExportData = + useCallback(async (): Promise => { + 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( + const response = await SaleReportApi.getHppPerKandangReport( params.area_id, params.location_id, params.kandang_id, @@ -260,26 +261,29 @@ const HppPerKandangTab = () => { params.period, params.sort_by, params.show_unrecorded - ) - ); + ); - const allExportData: HppPerKandangReport['rows'] = useMemo( - () => - isResponseSuccess(allDataForExport) - ? (allDataForExport?.data?.rows as HppPerKandangReport['rows']) || [] - : [], - [allDataForExport] - ); + return isResponseSuccess(response) ? response.data : null; + }, [tableFilterState]); // ===== EXPORT HANDLERS ===== - const handleExportExcel = useCallback(() => { - if (allExportData.length === 0) { - toast.error('Tidak ada data untuk diekspor.'); - return; - } - + const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { + const allDataForExport = await fetchAllExportData(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const allExportData = + allDataForExport.rows as HppPerKandangReport['rows']; + const totals = allExportData.reduce( (acc, item) => ({ total_remaining_chicken_birds: @@ -410,7 +414,7 @@ const HppPerKandangTab = () => { setIsExcelExportLoading(false); } }, [ - allExportData, + fetchAllExportData, tableFilterState, areaOptions, locationOptions, @@ -418,13 +422,19 @@ const HppPerKandangTab = () => { ]); const handleExportPDF = useCallback(async () => { - if (!hppPerKandang || !isResponseSuccess(hppPerKandang)) { - toast.error('Tidak ada data untuk diekspor.'); - return; - } - setIsPdfExportLoading(true); try { + const allDataForExport = await fetchAllExportData(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + const areaName = tableFilterState.area_id ? areaOptions.find( (opt) => opt.value === Number(tableFilterState.area_id) @@ -443,7 +453,7 @@ const HppPerKandangTab = () => { )?.label || 'Semua Kandang' : 'Semua Kandang'; - await generateHppPerKandangPDF(hppPerKandang.data, { + await generateHppPerKandangPDF(allDataForExport, { area_name: areaName, location_name: locationName, kandang_name: kandangName, @@ -461,7 +471,7 @@ const HppPerKandangTab = () => { setIsPdfExportLoading(false); } }, [ - hppPerKandang, + fetchAllExportData, tableFilterState, areaOptions, locationOptions, From 80fd75dfc14c1a4f7015b78f213bd95704bdbe3b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 14:14:27 +0700 Subject: [PATCH 16/50] refactor(FE-357): Rename API response and export fetch vars --- .../report/sale/tab/HppPerKandangTab.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 38ac1649..fa0e2df1 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -165,7 +165,7 @@ const HppPerKandangTab = () => { }, [tableFilterState.period]); // ===== DATA FETCHING ===== - const { data: hppPerKandangResponse, isLoading } = useSWR( + const { data: hppPerKandang, isLoading } = useSWR( isSubmitted ? () => { const params = { @@ -207,27 +207,24 @@ const HppPerKandangTab = () => { const data: HppPerKandangReport['rows'] = useMemo( () => - isResponseSuccess(hppPerKandangResponse) - ? (hppPerKandangResponse?.data?.rows as HppPerKandangReport['rows']) || - [] + isResponseSuccess(hppPerKandang) + ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] : [], - [hppPerKandangResponse] + [hppPerKandang] ); const summary = - isResponseSuccess(hppPerKandangResponse) && - hppPerKandangResponse?.data?.summary - ? hppPerKandangResponse.data.summary + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary + ? hppPerKandang.data.summary : undefined; const period = - isResponseSuccess(hppPerKandangResponse) && - hppPerKandangResponse?.data?.period - ? hppPerKandangResponse.data.period + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period + ? hppPerKandang.data.period : undefined; // ===== EXPORT DATA FETCHER ===== - const fetchAllExportData = + const hppPerKandangExport = useCallback(async (): Promise => { const params = { area_id: tableFilterState.area_id @@ -270,7 +267,7 @@ const HppPerKandangTab = () => { const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); try { - const allDataForExport = await fetchAllExportData(); + const allDataForExport = await hppPerKandangExport(); if ( !allDataForExport || From 6155929e14d530116a5a320e76fce8f24eed9537 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 17 Dec 2025 14:54:28 +0700 Subject: [PATCH 17/50] refactor(FE-357): Use hppPerKandangExport instead of fetchAllExportData --- src/components/pages/report/sale/tab/HppPerKandangTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index fa0e2df1..a92a5f75 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -411,7 +411,7 @@ const HppPerKandangTab = () => { setIsExcelExportLoading(false); } }, [ - fetchAllExportData, + hppPerKandangExport, tableFilterState, areaOptions, locationOptions, @@ -421,7 +421,7 @@ const HppPerKandangTab = () => { const handleExportPDF = useCallback(async () => { setIsPdfExportLoading(true); try { - const allDataForExport = await fetchAllExportData(); + const allDataForExport = await hppPerKandangExport(); if ( !allDataForExport || @@ -468,7 +468,7 @@ const HppPerKandangTab = () => { setIsPdfExportLoading(false); } }, [ - fetchAllExportData, + hppPerKandangExport, tableFilterState, areaOptions, locationOptions, From 9b2d98f7cec4a2b41abb29f6e7ead6af367a0210 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 08:59:07 +0700 Subject: [PATCH 18/50] feat(FE-355,357): Add egg columns to HPP table and disable sale tabs --- .../pages/report/sale/SaleReportTabs.tsx | 30 ++++---- .../report/sale/tab/HppPerKandangTab.tsx | 72 +++++++++++++++---- 2 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/components/pages/report/sale/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx index ed1ce0ac..988c16b2 100644 --- a/src/components/pages/report/sale/SaleReportTabs.tsx +++ b/src/components/pages/report/sale/SaleReportTabs.tsx @@ -5,21 +5,21 @@ import HppPerKandangTab from '@/components/pages/report/sale/tab/HppPerKandangTa const SaleReportTabs = () => { const tabs = [ - { - id: '1', - label: 'Penjualan Harian', - content: 'Penjualan Harian Tab', - }, - { - id: '2', - label: 'Transaksi Penjualan DO', - content: 'Transaksi Penjualan DO Tab', - }, - { - id: '3', - label: 'Perbandingan HPP Per Rentang BW', - content: 'Perbandingan HPP Per Rentang BW Tab', - }, + // { + // id: '1', + // label: 'Penjualan Harian', + // content: 'Penjualan Harian Tab', + // }, + // { + // id: '2', + // label: 'Transaksi Penjualan DO', + // content: 'Transaksi Penjualan DO Tab', + // }, + // { + // id: '3', + // label: 'Perbandingan HPP Per Rentang BW', + // content: 'Perbandingan HPP Per Rentang BW Tab', + // }, { id: '4', label: 'HPP Harian Kandang', diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index a92a5f75..8cf54194 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -536,6 +536,20 @@ const HppPerKandangTab = () => { }, 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?.average_weight_kg || 0)} +
+ ), + }, { id: 'remaining_chicken_birds', header: 'Sisa Ayam (Ekor)', @@ -565,17 +579,39 @@ const HppPerKandangTab = () => { ), }, { - id: 'avg_weight_kg', - header: 'Rata-Rata Bobot (KG)', - accessorKey: 'avg_weight_kg', + id: 'egg_production_pieces', + header: 'Produksi Telur (Butir)', + accessorKey: 'egg_production_pieces', cell: (props) => { - const value = props.row.original.avg_weight_kg; + const value = props.row.original.egg_production_pieces; return
{formatNumber(value)}
; }, footer: () => ( -
- {formatNumber(summary?.average_weight_kg || 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: () => ( +
-
+ ), + }, + { + 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: () => ( +
-
), }, { @@ -615,23 +651,31 @@ const HppPerKandangTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
- {formatCurrency(summary?.average_egg_hpp_rp_per_kg || 0)} -
+
-
), }, { id: 'hpp_rp', - header: 'HPP (RP)', + header: 'HPP Ayam (RP)', accessorKey: 'hpp_rp', cell: (props) => { const value = props.row.original.hpp_rp; return
{formatCurrency(value)}
; }, footer: () => ( -
- {formatCurrency(summary?.total_egg_value_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: () => ( +
-
), }, { From 481a643b3c0488803d1280cd273524b3d0ae9ce3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:11:21 +0700 Subject: [PATCH 19/50] refactor(FE-355): Use summary fallbacks for report footers --- .../report/sale/tab/HppPerKandangTab.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 8cf54194..5e8ebb63 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -560,7 +560,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(totals.total_remaining_chicken_birds)} + {formatNumber(summary?.total_remaining_chicken_birds || 0)}
), }, @@ -574,7 +574,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(totals.total_remaining_chicken_weight_kg)} + {formatNumber(summary?.total_remaining_chicken_weight_kg || 0)}
), }, @@ -587,7 +587,9 @@ const HppPerKandangTab = () => { return
{formatNumber(value)}
; }, footer: () => ( -
-
+
+ {formatNumber(summary?.total_egg_production_pieces || 0)} +
), }, { @@ -599,7 +601,9 @@ const HppPerKandangTab = () => { return
{formatNumber(value)}
; }, footer: () => ( -
-
+
+ {formatNumber(summary?.total_remaining_chicken_weight_kg || 0)} +
), }, { @@ -611,7 +615,9 @@ const HppPerKandangTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
-
+
+ {formatCurrency(summary?.total_egg_value_rp || 0)} +
), }, { @@ -675,7 +681,9 @@ const HppPerKandangTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
-
+
+ {formatCurrency(summary?.average_egg_hpp_rp_per_kg || 0)} +
), }, { @@ -688,7 +696,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(totals.total_remaining_value_rp)} + {formatCurrency(summary?.total_remaining_value_rp || 0)}
), }, From 40f2d0ba93e4336de48453907c3b8aa0f67fd3a8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:12:28 +0700 Subject: [PATCH 20/50] refactor(FE-355): Reorder egg_value_rp column in HppPerKandangTab --- .../report/sale/tab/HppPerKandangTab.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 5e8ebb63..d1fbefa3 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -606,20 +606,6 @@ const HppPerKandangTab = () => { ), }, - { - 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_egg_value_rp || 0)} -
- ), - }, { id: 'feed_suppliers', header: 'Feed (Supplier)', @@ -660,6 +646,20 @@ const HppPerKandangTab = () => {
-
), }, + { + 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_egg_value_rp || 0)} +
+ ), + }, { id: 'hpp_rp', header: 'HPP Ayam (RP)', From 320bc5224495e7fc112b0e6940f5a4c562430636 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:24:49 +0700 Subject: [PATCH 21/50] refactor(FE-355,357): Show HPP and average DOC totals in table footer --- .../report/sale/tab/HppPerKandangTab.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index d1fbefa3..82fe632b 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -27,9 +27,8 @@ 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; + total_hpp_rp: number; + total_average_doc_price_rp: number; } const HppPerKandangTab = () => { @@ -478,11 +477,14 @@ const HppPerKandangTab = () => { // ===== 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, + total_hpp_rp: data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0), + total_average_doc_price_rp: + data.length > 0 + ? data.reduce( + (acc, item) => acc + (item.average_doc_price_rp || 0), + 0 + ) / data.length + : 0, }; }, [summary]); @@ -643,7 +645,9 @@ const HppPerKandangTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
-
+
+ {formatCurrency(totals?.total_average_doc_price_rp || 0)} +
), }, { @@ -669,7 +673,9 @@ const HppPerKandangTab = () => { return
{formatCurrency(value)}
; }, footer: () => ( -
-
+
+ {formatCurrency(totals?.total_hpp_rp || 0)} +
), }, { From 69b4ca455ee50268ac1fc9e482a99e465c3b4806 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:33:13 +0700 Subject: [PATCH 22/50] refactor(FE-355): Guard HPP total calculation for empty data --- src/components/pages/report/sale/tab/HppPerKandangTab.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 82fe632b..14945264 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -477,7 +477,10 @@ const HppPerKandangTab = () => { // ===== TABLE COLUMNS DEFINITION ===== const totals: Totals = useMemo(() => { return { - total_hpp_rp: data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0), + total_hpp_rp: + data.length > 0 + ? data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0) + : 0, total_average_doc_price_rp: data.length > 0 ? data.reduce( From 3497a6346ce6f65dd6da49bcce8cdd42e0b322cb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:47:52 +0700 Subject: [PATCH 23/50] refactor(FE-357): Deduplicate totals and supplier aggregation --- .../report/sale/tab/HppPerKandangTab.tsx | 137 ++++++------------ 1 file changed, 46 insertions(+), 91 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 14945264..3b5cfa28 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -262,6 +262,43 @@ const HppPerKandangTab = () => { return isResponseSuccess(response) ? response.data : null; }, [tableFilterState]); + // ===== TABLE COLUMNS DEFINITION ===== + const totals: Totals = useMemo(() => { + return { + total_hpp_rp: + data.length > 0 + ? data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0) + : 0, + total_average_doc_price_rp: + data.length > 0 + ? data.reduce( + (acc, item) => acc + (item.average_doc_price_rp || 0), + 0 + ) / data.length + : 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]); + // ===== EXPORT HANDLERS ===== const handleExportExcel = useCallback(async () => { setIsExcelExportLoading(true); @@ -280,52 +317,7 @@ const HppPerKandangTab = () => { const allExportData = allDataForExport.rows as HppPerKandangReport['rows']; - 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_hpp_rp: acc.total_hpp_rp + (item.hpp_rp || 0), - average_doc_price_rp: - acc.average_doc_price_rp + (item.average_doc_price_rp || 0), - all_feed_suppliers: new Set([ - ...acc.all_feed_suppliers, - ...(item.feed_suppliers?.map((s) => s.alias || s.name) || []), - ]), - all_doc_suppliers: new Set([ - ...acc.all_doc_suppliers, - ...(item.doc_suppliers?.map((s) => s.alias || s.name) || []), - ]), - }), - { - total_remaining_chicken_birds: 0, - total_remaining_chicken_weight_kg: 0, - total_remaining_value_rp: 0, - total_hpp_rp: 0, - average_doc_price_rp: 0, - all_feed_suppliers: new Set(), - all_doc_suppliers: new Set(), - } - ); - - const avgWeight = - totals.total_remaining_chicken_birds > 0 - ? totals.total_remaining_chicken_weight_kg / - totals.total_remaining_chicken_birds - : 0; - const avgDocPrice = - allExportData.length > 0 - ? totals.average_doc_price_rp / allExportData.length - : 0; - const avgHpp = - totals.total_remaining_chicken_birds > 0 - ? totals.total_hpp_rp / totals.total_remaining_chicken_birds - : 0; + const summary = allDataForExport.summary; const excelData: { [key: string]: string | number }[] = allExportData.map( (item, index) => ({ @@ -352,14 +344,14 @@ const HppPerKandangTab = () => { 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)': avgWeight, - 'Feed (Supplier)': Array.from(totals.all_feed_suppliers).join(' | '), - 'DOC (Supplier)': Array.from(totals.all_doc_suppliers).join(' | '), - 'Rata-Rata Harga DOC (RP)': avgDocPrice, - 'HPP (RP)': avgHpp, - 'Nilai Nominal Sisa Ayam (RP)': totals.total_remaining_value_rp, + 'Sisa Ayam (Ekor)': summary?.total_remaining_chicken_birds || 0, + 'Sisa Ayam (KG)': summary?.total_remaining_chicken_weight_kg || 0, + 'Rata-Rata Bobot (KG)': summary?.average_weight_kg || 0, + 'Feed (Supplier)': allFeedSuppliers, + 'DOC (Supplier)': allDocSuppliers, + 'Rata-Rata Harga DOC (RP)': totals?.total_average_doc_price_rp || 0, + 'HPP (RP)': totals?.total_hpp_rp || 0, + 'Nilai Nominal Sisa Ayam (RP)': summary?.total_remaining_value_rp || 0, }); const worksheet = XLSX.utils.json_to_sheet(excelData); @@ -474,43 +466,6 @@ const HppPerKandangTab = () => { kandangOptions, ]); - // ===== TABLE COLUMNS DEFINITION ===== - const totals: Totals = useMemo(() => { - return { - total_hpp_rp: - data.length > 0 - ? data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0) - : 0, - total_average_doc_price_rp: - data.length > 0 - ? data.reduce( - (acc, item) => acc + (item.average_doc_price_rp || 0), - 0 - ) / data.length - : 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[] = [ { From cc3765abcd8806aff4a0acabfc17207d52344dee Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:50:11 +0700 Subject: [PATCH 24/50] refactor(FE-356): Add egg production and pricing columns --- .../report/sale/tab/HppPerKandangTab.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 3b5cfa28..8a6e8a9c 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -326,16 +326,20 @@ const HppPerKandangTab = () => { '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, - 'Rata-Rata Bobot (KG)': item.avg_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) => 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 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, }) ); @@ -344,13 +348,17 @@ const HppPerKandangTab = () => { 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, - 'Rata-Rata Bobot (KG)': summary?.average_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, - 'HPP (RP)': totals?.total_hpp_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, }); @@ -360,13 +368,17 @@ const HppPerKandangTab = () => { { 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 }, // Rata-Rata Bobot (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: 12 }, // HPP (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; From 4edd4f1285e036765249e660bc12a40c3a5a6653 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 09:57:21 +0700 Subject: [PATCH 25/50] feat(FE-356): Add egg production and HPP fields to PDF export --- .../sale/export/HppPerkandangExport.tsx | 128 +++++++++++++++--- 1 file changed, 112 insertions(+), 16 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index dc0c95f0..6fb0f914 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -165,7 +165,8 @@ interface WeightRangeGroup { } const groupDataByWeightRange = ( - data: HppPerKandangReport['rows'] + data: HppPerKandangReport['rows'], + summary: HppPerKandangReport['summary'] ): WeightRangeGroup[] => { const groups: { [key: string]: WeightRangeGroup; @@ -220,8 +221,12 @@ const groupDataByWeightRange = ( 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; + group.items.length > 0 + ? group.items.reduce( + (sum, item) => sum + item.average_doc_price_rp, + 0 + ) / group.items.length + : 0; }); return Object.values(groups).sort((a, b) => a.weight_min - b.weight_min); @@ -241,11 +246,27 @@ const getParameterText = (params: HppPerKandangExportParams['params']) => { return paramsText; }; +const getTotalsForExport = (data: HppPerKandangReport['rows']) => { + const totalHppRp = data.reduce((sum, item) => sum + (item.hpp_rp || 0), 0); + const avgDocPrice = + data.length > 0 + ? data.reduce((sum, item) => sum + (item.average_doc_price_rp || 0), 0) / + data.length + : 0; + + return { + total_hpp_rp: totalHppRp, + total_average_doc_price_rp: avgDocPrice, + }; +}; + const createPDFDocument = ( data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams['params'] ) => { - const groupedByWeightRange = groupDataByWeightRange(data.rows); + const summary = data.summary; + const exportTotals = getTotalsForExport(data.rows); + const groupedByWeightRange = groupDataByWeightRange(data.rows, summary); return ( @@ -283,6 +304,12 @@ const createPDFDocument = ( Rata-Rata Bobot (Kg) + + Produksi Telur (Butir) + + + Produksi Telur (Kg) + Feed (Supplier) @@ -292,8 +319,14 @@ const createPDFDocument = ( Rata-Rata Harga DOC + + Nilai Nominal Telur + - HPP + HPP Ayam + + + HPP Telur (RP/KG) Nominal Sisa @@ -332,6 +365,26 @@ const createPDFDocument = ( {formatNumber(group.totals.average_weight_kg)} + + + {formatNumber( + group.items.reduce( + (sum, item) => sum + (item.egg_production_pieces || 0), + 0 + ) + )} + + + + + {formatNumber( + group.items.reduce( + (sum, item) => sum + (item.egg_production_kg || 0), + 0 + ) + )} + + {group.totals.all_feed_suppliers.join(' | ')} @@ -341,15 +394,34 @@ const createPDFDocument = ( {formatNumber(group.totals.average_doc_price_rp)} + + + {formatNumber( + group.items.reduce( + (sum, item) => sum + (item.egg_value_rp || 0), + 0 + ) + )} + + - { - (group.totals.total_remaining_chicken_birds > 0 + {formatNumber( + group.totals.total_remaining_chicken_birds > 0 ? group.totals.total_hpp_rp / - group.totals.total_remaining_chicken_birds - : 0, - 2) - } + group.totals.total_remaining_chicken_birds + : 0 + )} + + + + + {formatNumber( + group.items.reduce( + (sum, item) => sum + (item.egg_hpp_rp_per_kg || 0), + 0 + ) / group.items.length || 0 + )} @@ -378,14 +450,20 @@ const createPDFDocument = ( Rentang BW + + Rata-Rata Bobot (Kg) + Sisa Ekor Sisa Kg - - Rata-Rata Bobot (Kg) + + Produksi Telur (Butir) + + + Produksi Telur (Kg) Feed (Supplier) @@ -396,8 +474,14 @@ const createPDFDocument = ( Rata-Rata Harga DOC + + Nilai Nominal Telur + - HPP + HPP Ayam + + + HPP Telur (RP/KG) Nominal Sisa @@ -427,14 +511,20 @@ const createPDFDocument = ( {item.weight_range.weight_max.toFixed(2)} + + {formatNumber(item.avg_weight_kg)} + {formatNumber(item.remaining_chicken_birds)} {formatNumber(item.remaining_chicken_weight_kg)} - - {formatNumber(item.avg_weight_kg)} + + {formatNumber(item.egg_production_pieces)} + + + {formatNumber(item.egg_production_kg)} @@ -453,9 +543,15 @@ const createPDFDocument = ( {formatNumber(item.average_doc_price_rp)} + + {formatNumber(item.egg_value_rp)} + {formatNumber(item.hpp_rp)} + + {formatNumber(item.egg_hpp_rp_per_kg)} + {formatNumber(item.remaining_value_rp)} From 83fc92d48be4eb0399910ea436a30242dc89284f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 11:12:30 +0700 Subject: [PATCH 26/50] refactor(FE-356): Refactor weight-range grouping and format currency --- .../sale/export/HppPerkandangExport.tsx | 230 +++++++----------- 1 file changed, 83 insertions(+), 147 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 6fb0f914..d6bd3e25 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -10,7 +10,7 @@ import { pdf, } from '@react-pdf/renderer'; import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; -import { formatDate, formatNumber } from '@/lib/helper'; +import { formatDate, formatNumber, formatCurrency } from '@/lib/helper'; Font.register({ family: 'Helvetica', @@ -148,88 +148,72 @@ interface HppPerKandangExportParams { }; } -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'], - summary: HppPerKandangReport['summary'] -): WeightRangeGroup[] => { - const groups: { - [key: string]: WeightRangeGroup; - } = {}; +const rekapitulasiData = (data: HppPerKandangReport['rows']) => { + const groups: { [key: string]: HppPerKandangReport['rows'] } = {}; 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] = []; } - - 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); - } - }); + groups[key].push(item); }); - 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.length > 0 - ? group.items.reduce( - (sum, item) => sum + item.average_doc_price_rp, - 0 - ) / group.items.length - : 0; - }); - - return Object.values(groups).sort((a, b) => a.weight_min - b.weight_min); + return Object.entries(groups) + .map(([key, items]) => ({ + weight_min: items[0].weight_range.weight_min, + weight_max: items[0].weight_range.weight_max, + items, + total_remaining_chicken_birds: items.reduce( + (sum, item) => sum + item.remaining_chicken_birds, + 0 + ), + total_remaining_chicken_weight_kg: items.reduce( + (sum, item) => sum + item.remaining_chicken_weight_kg, + 0 + ), + average_weight_kg: + items.reduce((sum, item) => sum + item.remaining_chicken_weight_kg, 0) / + items.reduce((sum, item) => sum + item.remaining_chicken_birds, 0), + total_hpp_rp: items.reduce((sum, item) => sum + item.hpp_rp, 0), + total_remaining_value_rp: items.reduce( + (sum, item) => sum + item.remaining_value_rp, + 0 + ), + total_egg_production_pieces: items.reduce( + (sum, item) => sum + (item.egg_production_pieces || 0), + 0 + ), + total_egg_production_kg: items.reduce( + (sum, item) => sum + (item.egg_production_kg || 0), + 0 + ), + total_egg_value_rp: items.reduce( + (sum, item) => sum + (item.egg_value_rp || 0), + 0 + ), + average_egg_hpp_rp_per_kg: + items.reduce((sum, item) => sum + (item.egg_hpp_rp_per_kg || 0), 0) / + items.length, + average_doc_price_rp: + items.reduce((sum, item) => sum + item.average_doc_price_rp, 0) / + items.length, + all_feed_suppliers: [ + ...new Set( + items.flatMap( + (item) => item.feed_suppliers?.map((s) => s.alias || s.name) || [] + ) + ), + ], + all_doc_suppliers: [ + ...new Set( + items.flatMap( + (item) => item.doc_suppliers?.map((s) => s.alias || s.name) || [] + ) + ), + ], + })) + .sort((a, b) => a.weight_min - b.weight_min); }; const getParameterText = (params: HppPerKandangExportParams['params']) => { @@ -246,27 +230,11 @@ const getParameterText = (params: HppPerKandangExportParams['params']) => { return paramsText; }; -const getTotalsForExport = (data: HppPerKandangReport['rows']) => { - const totalHppRp = data.reduce((sum, item) => sum + (item.hpp_rp || 0), 0); - const avgDocPrice = - data.length > 0 - ? data.reduce((sum, item) => sum + (item.average_doc_price_rp || 0), 0) / - data.length - : 0; - - return { - total_hpp_rp: totalHppRp, - total_average_doc_price_rp: avgDocPrice, - }; -}; - const createPDFDocument = ( data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams['params'] ) => { - const summary = data.summary; - const exportTotals = getTotalsForExport(data.rows); - const groupedByWeightRange = groupDataByWeightRange(data.rows, summary); + const rekapitulasiByWeightRange = rekapitulasiData(data.rows); return ( @@ -334,12 +302,12 @@ const createPDFDocument = ( {/* Table Body - Rekapitulasi */} - {groupedByWeightRange.map((group, index) => ( + {rekapitulasiByWeightRange.map((group, index) => ( - {formatNumber(group.totals.total_remaining_chicken_birds)} + {formatNumber(group.total_remaining_chicken_birds)} - {formatNumber( - group.totals.total_remaining_chicken_weight_kg - )} + {formatNumber(group.total_remaining_chicken_weight_kg)} - {formatNumber(group.totals.average_weight_kg)} + {formatNumber(group.average_weight_kg)} - - {formatNumber( - group.items.reduce( - (sum, item) => sum + (item.egg_production_pieces || 0), - 0 - ) - )} - + {formatNumber(group.total_egg_production_pieces)} - - {formatNumber( - group.items.reduce( - (sum, item) => sum + (item.egg_production_kg || 0), - 0 - ) - )} - + {formatNumber(group.total_egg_production_kg)} - {group.totals.all_feed_suppliers.join(' | ')} + {group.all_feed_suppliers.join(' | ')} - {group.totals.all_doc_suppliers.join(' | ')} + {group.all_doc_suppliers.join(' | ')} - {formatNumber(group.totals.average_doc_price_rp)} + {formatCurrency(group.average_doc_price_rp)} - - {formatNumber( - group.items.reduce( - (sum, item) => sum + (item.egg_value_rp || 0), - 0 - ) - )} - + {formatCurrency(group.total_egg_value_rp)} - {formatNumber( - group.totals.total_remaining_chicken_birds > 0 - ? group.totals.total_hpp_rp / - group.totals.total_remaining_chicken_birds + {formatCurrency( + group.total_remaining_chicken_birds > 0 + ? group.total_hpp_rp / + group.total_remaining_chicken_birds : 0 )} - - {formatNumber( - group.items.reduce( - (sum, item) => sum + (item.egg_hpp_rp_per_kg || 0), - 0 - ) / group.items.length || 0 - )} - + {formatCurrency(group.average_egg_hpp_rp_per_kg)} - - {formatNumber(group.totals.total_remaining_value_rp)} - + {formatCurrency(group.total_remaining_value_rp)} ))} @@ -541,19 +477,19 @@ const createPDFDocument = ( - {formatNumber(item.average_doc_price_rp)} + {formatCurrency(item.average_doc_price_rp)} - {formatNumber(item.egg_value_rp)} + {formatCurrency(item.egg_value_rp)} - {formatNumber(item.hpp_rp)} + {formatCurrency(item.hpp_rp)} - {formatNumber(item.egg_hpp_rp_per_kg)} + {formatCurrency(item.egg_hpp_rp_per_kg)} - {formatNumber(item.remaining_value_rp)} + {formatCurrency(item.remaining_value_rp)} ))} From f844c9ff2ccc6ddcde0e8fab62558ab8e98930e5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 11:19:23 +0700 Subject: [PATCH 27/50] refactor(FE-357): Use relative URL for marketing sale reports --- src/services/api/report/marketing-sale.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index e15d2a8b..ba8fe481 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -44,7 +44,13 @@ export class MarketingSaleReportService extends BaseApiService< } } -// TODO: REPLACE WITH PRODUCTION URL export const SaleReportApi = new MarketingSaleReportService( - 'http://localhost:4010/api/reports/marketings' + 'reports/marketings' ); + +/* For local testing purpose only +export const SaleReportApi = new MarketingSaleReportService( + 'http://localhost:4010/api/reports/marketings' + 'reports/marketings' + ); +*/ From 843fa6ee7a9f95ceb53deeb90ce877b3ab736e96 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 12:56:05 +0700 Subject: [PATCH 28/50] refactor(FE-357): Make area/location/kandang filters multi-select --- .../report/sale/tab/HppPerKandangTab.tsx | 207 +++++++++++------- src/services/api/report/marketing-sale.ts | 15 +- 2 files changed, 129 insertions(+), 93 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 8a6e8a9c..9a9ddde7 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -43,9 +43,9 @@ const HppPerKandangTab = () => { // ===== TABLE FILTER STATE ===== const { state: tableFilterState, updateFilter } = useTableFilter({ initial: { - area_id: '', - location_id: '', - kandang_id: '', + area_id: [] as string[], + location_id: [] as string[], + kandang_id: [] as string[], weight_min: '', weight_max: '', period: '', @@ -78,8 +78,11 @@ const HppPerKandangTab = () => { const areaChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('area_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'area_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -87,8 +90,11 @@ const HppPerKandangTab = () => { const locationChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('location_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'location_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -96,8 +102,11 @@ const HppPerKandangTab = () => { const kandangChangeHandler = useCallback( (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - updateFilter('kandang_id', newVal?.value ? String(newVal.value) : ''); + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'kandang_id', + arr.map((v) => String((v as OptionType).value)) + ); setIsSubmitted(false); }, [updateFilter] @@ -144,9 +153,9 @@ const HppPerKandangTab = () => { ); const resetFilters = useCallback(() => { - updateFilter('area_id', ''); - updateFilter('location_id', ''); - updateFilter('kandang_id', ''); + updateFilter('area_id', []); + updateFilter('location_id', []); + updateFilter('kandang_id', []); updateFilter('weight_min', ''); updateFilter('weight_max', ''); updateFilter('period', ''); @@ -168,15 +177,18 @@ const HppPerKandangTab = () => { 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, + 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 ? Number(tableFilterState.weight_min) : undefined, @@ -226,15 +238,18 @@ const HppPerKandangTab = () => { const hppPerKandangExport = useCallback(async (): Promise => { 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, + 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 ? Number(tableFilterState.weight_min) : undefined, @@ -386,23 +401,38 @@ const HppPerKandangTab = () => { 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 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 - ? locationOptions.find( - (opt) => opt.value === Number(tableFilterState.location_id) - )?.label || 'Semua Lokasi' - : 'Semua Lokasi'; + 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 - ? kandangOptions.find( - (opt) => opt.value === Number(tableFilterState.kandang_id) - )?.label || 'Semua Kandang' - : 'Semua Kandang'; + 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'; const filename = `Laporan_HPP_Per_Kandang_${areaName}_${locationName}_${kandangName}_${tableFilterState.period}.xlsx`; @@ -435,23 +465,38 @@ const HppPerKandangTab = () => { return; } - const areaName = tableFilterState.area_id - ? areaOptions.find( - (opt) => opt.value === Number(tableFilterState.area_id) - )?.label || 'Semua Area' - : 'Semua Area'; + 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 - ? locationOptions.find( - (opt) => opt.value === Number(tableFilterState.location_id) - )?.label || 'Semua Lokasi' - : 'Semua Lokasi'; + 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 - ? kandangOptions.find( - (opt) => opt.value === Number(tableFilterState.kandang_id) - )?.label || 'Semua Kandang' - : 'Semua Kandang'; + 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, @@ -716,15 +761,13 @@ const HppPerKandangTab = () => { - option.value === Number(tableFilterState.area_id) - ) || null - : null - } + value={areaOptions.filter((opt) => + (tableFilterState.area_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={areaChangeHandler} isLoading={isLoadingAreas} isClearable @@ -732,15 +775,13 @@ const HppPerKandangTab = () => { - option.value === Number(tableFilterState.location_id) - ) || null - : null - } + value={locationOptions.filter((opt) => + (tableFilterState.location_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={locationChangeHandler} isLoading={isLoadingLocations} isClearable @@ -748,15 +789,13 @@ const HppPerKandangTab = () => { - option.value === Number(tableFilterState.kandang_id) - ) || null - : null - } + value={kandangOptions.filter((opt) => + (tableFilterState.kandang_id || []) + .map(String) + .includes(String(opt.value)) + )} onChange={kandangChangeHandler} isLoading={isLoadingKandangs} isClearable diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index ba8fe481..4422957e 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -12,9 +12,9 @@ export class MarketingSaleReportService extends BaseApiService< } async getHppPerKandangReport( - area_id?: number, - location_id?: number, - kandang_id?: number, + area_id?: string, + location_id?: string, + kandang_id?: string, weight_min?: number, weight_max?: number, period?: string, @@ -48,9 +48,6 @@ export const SaleReportApi = new MarketingSaleReportService( 'reports/marketings' ); -/* For local testing purpose only -export const SaleReportApi = new MarketingSaleReportService( - 'http://localhost:4010/api/reports/marketings' - 'reports/marketings' - ); -*/ +// export const SaleReportApi = new MarketingSaleReportService( +// 'http://localhost:4010/api/reports/marketings' +// ); From 6b30457ec26c2ba70e8ccc934fdefb5cec6ced9c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:02:16 +0700 Subject: [PATCH 29/50] feat(FE-356): Include filter details in export header --- .../sale/export/HppPerkandangExport.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index d6bd3e25..12d0a697 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -219,11 +219,37 @@ const rekapitulasiData = (data: HppPerKandangReport['rows']) => { const getParameterText = (params: HppPerKandangExportParams['params']) => { const paramsText = []; + if (params.area_name && params.area_name !== 'Semua Area') { + paramsText.push(`Area: ${params.area_name}`); + } + + if (params.location_name && params.location_name !== 'Semua Lokasi') { + paramsText.push(`Lokasi: ${params.location_name}`); + } + + if (params.kandang_name && params.kandang_name !== 'Semua Kandang') { + paramsText.push(`Kandang: ${params.kandang_name}`); + } + if (params.period) { const formattedDate = formatDate(params.period, 'DD MMM YYYY'); paramsText.push(`Tanggal: ${formattedDate}`); } + if (params.weight_min || params.weight_max) { + const weightRange = + params.weight_min && params.weight_max + ? `${params.weight_min} - ${params.weight_max} kg` + : params.weight_min + ? `≥ ${params.weight_min} kg` + : `≤ ${params.weight_max} kg`; + paramsText.push(`Rentang Bobot: ${weightRange}`); + } + + if (params.show_unrecorded === 'true') { + paramsText.push('Tampilkan: Tanpa Recording'); + } + const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm'); paramsText.push(`Dicetak: ${currentDate}`); From fa7824224c6a0ec08e4fc724b12bf038cf7b417b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:05:01 +0700 Subject: [PATCH 30/50] refactor(FE-356): Use timestamped filename for HPP export --- .../report/sale/tab/HppPerKandangTab.tsx | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 9a9ddde7..7ccfd439 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -14,7 +14,7 @@ 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 { formatCurrency, formatDate, 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'; @@ -401,40 +401,7 @@ const HppPerKandangTab = () => { const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang'); - 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'; - - const filename = `Laporan_HPP_Per_Kandang_${areaName}_${locationName}_${kandangName}_${tableFilterState.period}.xlsx`; + const filename = `laporan-hpp-harian-kandang-${tableFilterState.period}-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); From ceae338c73dda2f163114dee5808a2fdc393f2d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 13:10:21 +0700 Subject: [PATCH 31/50] refactor(FE-356): Include period in export filenames --- .../pages/report/sale/export/HppPerkandangExport.tsx | 5 ++++- src/components/pages/report/sale/tab/HppPerKandangTab.tsx | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 12d0a697..72e7cbaa 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -537,7 +537,10 @@ export const generateHppPerKandangPDF = async ( 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`; + + const period = params.period || formatDate(new Date(), 'YYYY-MM-DD'); + link.download = `laporan-hpp-harian-kandang-periode-${period}.pdf`; + document.body.appendChild(link); link.click(); document.body.removeChild(link); diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7ccfd439..c9456a8c 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -401,7 +401,7 @@ const HppPerKandangTab = () => { const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang'); - const filename = `laporan-hpp-harian-kandang-${tableFilterState.period}-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; XLSX.writeFile(workbook, filename); toast.success('Excel berhasil dibuat dan diunduh.'); From 00e0126e42f48a94ace2bb0394f9f9ea80eda5fa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Thu, 18 Dec 2025 20:35:47 +0700 Subject: [PATCH 32/50] refactor(FE-357): Use string weights and parse floats for filters --- .../report/sale/tab/HppPerKandangTab.tsx | 20 ++++++------------- src/services/api/report/marketing-sale.ts | 4 ++-- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index c9456a8c..7c9847c8 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -117,7 +117,7 @@ const HppPerKandangTab = () => { >( (e) => { const val = e.target.value; - updateFilter('weight_min', val ? String(parseInt(val, 10)) : ''); + updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); setIsSubmitted(false); }, [updateFilter] @@ -128,7 +128,7 @@ const HppPerKandangTab = () => { >( (e) => { const val = e.target.value; - updateFilter('weight_max', val ? String(parseInt(val, 10)) : ''); + updateFilter('weight_max', val ? String(parseFloat(val) || 0) : ''); setIsSubmitted(false); }, [updateFilter] @@ -189,12 +189,8 @@ const HppPerKandangTab = () => { tableFilterState.kandang_id.length > 0 ? tableFilterState.kandang_id.join(',') : undefined, - weight_min: tableFilterState.weight_min - ? Number(tableFilterState.weight_min) - : undefined, - weight_max: tableFilterState.weight_max - ? Number(tableFilterState.weight_max) - : 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, @@ -250,12 +246,8 @@ const HppPerKandangTab = () => { tableFilterState.kandang_id.length > 0 ? tableFilterState.kandang_id.join(',') : undefined, - weight_min: tableFilterState.weight_min - ? Number(tableFilterState.weight_min) - : undefined, - weight_max: tableFilterState.weight_max - ? Number(tableFilterState.weight_max) - : 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, diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts index 4422957e..bb9c1f49 100644 --- a/src/services/api/report/marketing-sale.ts +++ b/src/services/api/report/marketing-sale.ts @@ -15,8 +15,8 @@ export class MarketingSaleReportService extends BaseApiService< area_id?: string, location_id?: string, kandang_id?: string, - weight_min?: number, - weight_max?: number, + weight_min?: string, + weight_max?: string, period?: string, sort_by?: string, show_unrecorded?: boolean, From 7f694c72988a6b80e88927ca235053c685443dd0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 19 Dec 2025 10:17:08 +0700 Subject: [PATCH 33/50] chore(FE): Bump Next.js to 15.5.9 --- package-lock.json | 28 +++++++++++++++++++--------- package.json | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0212474..c0bf87aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.7", + "next": "15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -1082,9 +1082,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1855,6 +1855,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1924,6 +1925,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2447,6 +2449,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3060,7 +3063,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.5.8", @@ -3516,6 +3520,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3689,6 +3694,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5654,12 +5660,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -6167,6 +6173,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6197,6 +6204,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7083,6 +7091,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7250,6 +7259,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 52fc6ce2..f5bd2d0f 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.7", + "next": "15.5.9", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", From d7b828cb47c2eb2ebbd6dd06e06551ac19a71cae Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 19 Dec 2025 10:24:16 +0700 Subject: [PATCH 34/50] chore(FE): Add xlsx@0.20.3 from SheetJS CDN --- package-lock.json | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/package-lock.json b/package-lock.json index c0bf87aa..0c0c75ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -7535,6 +7536,18 @@ "node": ">=0.10.0" } }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index f5bd2d0f..d0b99b80 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, From 7259de8b1492fd1d2c251b7a3d8f3125271804fb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 19 Dec 2025 12:56:25 +0700 Subject: [PATCH 35/50] feat(FE): Add renderCustomRow prop to Table --- src/components/Table.tsx | 52 +++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 9feb33e2..9791dd59 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -60,6 +60,12 @@ export interface TableProps { renderFooter?: boolean; withCheckbox?: boolean; rowOptions?: number[]; + /** + * Custom row renderer. Should return a complete
element or null. + * This gives full control over the row structure including colspan. + * Return null to render the default row. + */ + renderCustomRow?: (row: Row) => ReactNode | null; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -112,6 +118,7 @@ const Table = ({ renderFooter = false, withCheckbox = false, rowOptions = [10, 20, 50, 100], + renderCustomRow, }: TableProps) => { const isServerSideTable = totalItems !== undefined && @@ -305,24 +312,35 @@ const Table = ({ - {table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - ))} - - ))} + if (customRowContent) { + return renderCustomRow?.(row); + } + + return ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ); + })} {renderFooter && ( From c36d1ee1538516dad313852d391dff2c52037adf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 19 Dec 2025 13:40:41 +0700 Subject: [PATCH 36/50] feat(FE-355): Add custom row renderer to HppPerKandangTab --- .../report/sale/tab/HppPerKandangTab.tsx | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7c9847c8..cc1b3945 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -13,8 +13,8 @@ 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, formatDate, formatNumber } from '@/lib/helper'; +import { ColumnDef, Row } 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'; @@ -684,6 +684,27 @@ const HppPerKandangTab = () => { return tableColumns; }; + // ===== CUSTOM ROW RENDERER ===== + const renderCustomRow = useCallback( + (row: Row) => { + if (row.index === data.length - 1) { + return ( + + + + ); + } + + return null; + }, + [data] + ); + return (
{ data={data} columns={getTableColumns()} renderFooter={data.length > 0} + renderCustomRow={renderCustomRow} className={{ containerClassName: 'w-full mt-6', tableWrapperClassName: 'overflow-x-auto mt-4', From da5a577fde3b83c33fe151f3f38516977e118396 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 19 Dec 2025 13:46:02 +0700 Subject: [PATCH 37/50] refactor(FE-357): Add key to summary table row --- src/components/pages/report/sale/tab/HppPerKandangTab.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index cc1b3945..e7081f84 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -689,7 +689,10 @@ const HppPerKandangTab = () => { (row: Row) => { if (row.index === data.length - 1) { return ( -
+ { > Rekapitulasi per rentang bobot - - ); + , + ]; + + if (perWeightRangeSummary.length > 0) { + rows.push( + + + + ); + } + + return rows; } return null; }, - [data] + [data, perWeightRangeSummary] ); return ( From 1ac35691ff1fa3716c75397e59b0d3e5999b5ea3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 09:32:35 +0700 Subject: [PATCH 41/50] refactor(FE-355): Move td classes into tr with [&_td] utilities --- .../report/sale/tab/HppPerKandangTab.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index f7e92e71..ff35eb45 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -766,43 +766,40 @@ const HppPerKandangTab = () => { (item: HppPerKandangPerWeightRange, index = 0) => ( - - - - + + + - - - + + - - - - + From a0e63ea2d461400a3e59a3d8f1e9b0dc98cb88d5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 09:37:42 +0700 Subject: [PATCH 42/50] refactor(FE-355): Use flexRender and prepend default row in table --- .../report/sale/tab/HppPerKandangTab.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index ff35eb45..5248521f 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -13,7 +13,7 @@ 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 } from '@tanstack/react-table'; +import { ColumnDef, Row, flexRender } from '@tanstack/react-table'; import { formatCurrency, formatNumber } from '@/lib/helper'; import { HppPerKandangReport, @@ -739,7 +739,23 @@ const HppPerKandangTab = () => { const renderCustomRow = useCallback( (row: Row) => { if (row.index === data.length - 1) { - const rows = [ + const defaultRow = ( + + {row.getVisibleCells().map((cell) => ( + + ))} + + ); + + const customRows = [ { ]; if (perWeightRangeSummary.length > 0) { - rows.push( + customRows.push( @@ -770,61 +770,49 @@ const HppPerKandangTab = () => { ]; if (perWeightRangeSummary.length > 0) { - customRows.push( - - - + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index = 0) => { + customRows.push( + + + + + + + + + + + + + + + + + + ); + } ); } From 18ca7d8a594d89ece2ea6e50b683ed8107961089 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 09:50:19 +0700 Subject: [PATCH 44/50] refactor(FE-357): Reference summary.total in footers --- .../report/sale/tab/HppPerKandangTab.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 1748a6c7..ef8ac107 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -29,7 +29,6 @@ import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; -import { Supplier } from '@/types/api/master-data/supplier'; interface Totals { total_hpp_rp: number; @@ -565,7 +564,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.average_weight_kg || 0)} + {formatNumber(summary?.total?.average_weight_kg || 0)}
), }, @@ -579,7 +578,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total_remaining_chicken_birds || 0)} + {formatNumber(summary?.total?.total_remaining_chicken_birds || 0)}
), }, @@ -593,7 +592,9 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total_remaining_chicken_weight_kg || 0)} + {formatNumber( + summary?.total?.total_remaining_chicken_weight_kg || 0 + )}
), }, @@ -607,7 +608,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total_egg_production_pieces || 0)} + {formatNumber(summary?.total?.total_egg_production_pieces || 0)}
), }, @@ -621,7 +622,9 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total_remaining_chicken_weight_kg || 0)} + {formatNumber( + summary?.total?.total_remaining_chicken_weight_kg || 0 + )}
), }, @@ -685,7 +688,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total_egg_value_rp || 0)} + {formatCurrency(summary?.total?.total_egg_value_rp || 0)}
), }, @@ -713,7 +716,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.average_egg_hpp_rp_per_kg || 0)} + {formatCurrency(summary?.total?.average_egg_hpp_rp_per_kg || 0)}
), }, @@ -727,7 +730,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total_remaining_value_rp || 0)} + {formatCurrency(summary?.total?.total_remaining_value_rp || 0)}
), }, From 9e0631a41597e8909bf9f1eed44b5729328df007 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 10:13:48 +0700 Subject: [PATCH 45/50] feat(FE-355,357): Use summary totals and show egg metrics --- .../report/sale/tab/HppPerKandangTab.tsx | 69 +++++-------------- src/types/api/report/hpp-per-kandang.d.ts | 6 ++ 2 files changed, 22 insertions(+), 53 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index ef8ac107..cf6f100f 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -30,11 +30,6 @@ 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); @@ -285,46 +280,6 @@ const HppPerKandangTab = () => { }, [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) => { @@ -404,9 +359,9 @@ const HppPerKandangTab = () => { '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, + 'Rata-Rata Harga DOC (RP)': summary?.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 Ayam (RP)': summary?.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, }); @@ -674,7 +629,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(totals?.total_average_doc_price_rp || 0)} + {formatCurrency(summary?.total?.total_average_doc_price_rp || 0)}
), }, @@ -702,7 +657,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(totals?.total_hpp_rp || 0)} + {formatCurrency(summary?.total?.total_hpp_rp || 0)}
), }, @@ -792,8 +747,12 @@ const HppPerKandangTab = () => {
- - + + - + - + diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts index ad3f4e0e..824a3837 100644 --- a/src/types/api/report/hpp-per-kandang.d.ts +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -32,6 +32,8 @@ export type HppPerKandangSummaryTotal = { total_egg_production_kg: number; average_egg_hpp_rp_per_kg: number; total_egg_value_rp: number; + total_hpp_rp: number; + total_average_doc_price_rp: number; }; export type HppPerKandangPerWeightRange = { @@ -44,6 +46,10 @@ export type HppPerKandangPerWeightRange = { remaining_chicken_birds: number; remaining_chicken_weight_kg: number; avg_weight_kg: number; + egg_production_pieces: number; + egg_production_kg: number; + egg_hpp_rp_per_kg: number; + egg_value_rp: number; feed_suppliers: Supplier[]; doc_suppliers: Supplier[]; average_doc_price_rp: number; From 478e9eb541d64e8c5a21cd85a4afb65a0f41ca5c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 10:18:50 +0700 Subject: [PATCH 46/50] refactor(FE-356): Use summaryTotal and memoize per-weight summary --- .../report/sale/tab/HppPerKandangTab.tsx | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index cf6f100f..330cfd8f 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -219,21 +219,19 @@ const HppPerKandangTab = () => { [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 perWeightRangeSummary = useMemo( + () => + isResponseSuccess(hppPerKandang) && + hppPerKandang?.data?.summary?.per_weight_range + ? hppPerKandang.data.summary.per_weight_range + : [], + [hppPerKandang] + ); const period = isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.period @@ -318,7 +316,7 @@ const HppPerKandangTab = () => { const allExportData = allDataForExport.rows as HppPerKandangReport['rows']; - const summary = allDataForExport.summary; + const summaryTotal = allDataForExport.summary.total; const excelData: { [key: string]: string | number }[] = allExportData.map( (item: HppPerKandangRow, index: number) => ({ @@ -352,18 +350,21 @@ const HppPerKandangTab = () => { 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, + 'Rata-Rata Bobot (KG)': summaryTotal?.average_weight_kg || 0, + 'Sisa Ayam (Ekor)': summaryTotal?.total_remaining_chicken_birds || 0, + 'Sisa Ayam (KG)': summaryTotal?.total_remaining_chicken_weight_kg || 0, + 'Produksi Telur (Butir)': + summaryTotal?.total_egg_production_pieces || 0, + 'Produksi Telur (KG)': summaryTotal?.total_egg_production_kg || 0, 'Feed (Supplier)': allFeedSuppliers, 'DOC (Supplier)': allDocSuppliers, - 'Rata-Rata Harga DOC (RP)': summary?.total_average_doc_price_rp || 0, - 'Nilai Nominal Telur (RP)': summary?.total_egg_value_rp || 0, - 'HPP Ayam (RP)': summary?.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, + 'Rata-Rata Harga DOC (RP)': + summaryTotal?.total_average_doc_price_rp || 0, + 'Nilai Nominal Telur (RP)': summaryTotal?.total_egg_value_rp || 0, + 'HPP Ayam (RP)': summaryTotal?.total_hpp_rp || 0, + 'HPP Telur (RP/KG)': summaryTotal?.average_egg_hpp_rp_per_kg || 0, + 'Nilai Nominal Sisa Ayam (RP)': + summaryTotal?.total_remaining_value_rp || 0, }); const worksheet = XLSX.utils.json_to_sheet(excelData); @@ -519,7 +520,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total?.average_weight_kg || 0)} + {formatNumber(summaryTotal?.average_weight_kg || 0)}
), }, @@ -533,7 +534,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total?.total_remaining_chicken_birds || 0)} + {formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)}
), }, @@ -547,9 +548,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber( - summary?.total?.total_remaining_chicken_weight_kg || 0 - )} + {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
), }, @@ -563,7 +562,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber(summary?.total?.total_egg_production_pieces || 0)} + {formatNumber(summaryTotal?.total_egg_production_pieces || 0)}
), }, @@ -577,9 +576,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatNumber( - summary?.total?.total_remaining_chicken_weight_kg || 0 - )} + {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
), }, @@ -629,7 +626,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total?.total_average_doc_price_rp || 0)} + {formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)}
), }, @@ -643,7 +640,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total?.total_egg_value_rp || 0)} + {formatCurrency(summaryTotal?.total_egg_value_rp || 0)}
), }, @@ -657,7 +654,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total?.total_hpp_rp || 0)} + {formatCurrency(summaryTotal?.total_hpp_rp || 0)}
), }, @@ -671,7 +668,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total?.average_egg_hpp_rp_per_kg || 0)} + {formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)}
), }, @@ -685,7 +682,7 @@ const HppPerKandangTab = () => { }, footer: () => (
- {formatCurrency(summary?.total?.total_remaining_value_rp || 0)} + {formatCurrency(summaryTotal?.total_remaining_value_rp || 0)}
), }, From 982a5d0d11c0d140dbab81418736a7f8ec257478 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 10:25:40 +0700 Subject: [PATCH 47/50] feat(FE-356): Display egg production and HPP in PDF export --- .../report/sale/export/HppPerkandangExport.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 790efef7..6575dd50 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -290,10 +290,10 @@ const createPDFDocument = ( {formatNumber(group.avg_weight_kg)} - - + {formatNumber(group.egg_production_pieces)} - - + {formatNumber(group.egg_production_kg)} @@ -322,16 +322,10 @@ const createPDFDocument = ( - - - {formatCurrency( - group.remaining_chicken_birds > 0 - ? group.hpp_rp / group.remaining_chicken_birds - : 0 - )} - + {formatCurrency(group.hpp_rp)} - - + {formatCurrency(group.egg_hpp_rp_per_kg)} {formatCurrency(group.remaining_value_rp)} From 804aa700d32d0616ebbdc0e456be54e708af4800 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 10:26:18 +0700 Subject: [PATCH 48/50] feat(FE-356): Display egg production and HPP in PDF export --- src/components/pages/report/sale/export/HppPerkandangExport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 6575dd50..30f08b68 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -319,7 +319,7 @@ const createPDFDocument = ( {formatCurrency(group.average_doc_price_rp)} - - + {formatCurrency(group.egg_value_rp)} {formatCurrency(group.hpp_rp)} From a5e79570c5b2d7d98858ee46df13f3b394a8f4a5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 10:30:11 +0700 Subject: [PATCH 49/50] refactor(FE-356): Clarify 'Sisa Kg' label to specify Ayam --- src/components/pages/report/sale/export/HppPerkandangExport.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 30f08b68..0a712a6c 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -359,7 +359,7 @@ const createPDFDocument = ( Sisa Ekor - Sisa Kg + Sisa Kg (Ayam) Produksi Telur (Butir) From 346d6554066f3e5045934c97d929df8a5ecc0896 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 09:25:26 +0700 Subject: [PATCH 50/50] refactor(FE): Upgrade xlsx and add HPP per kandang tab --- package-lock.json | 113 +++--------------- package.json | 4 +- .../pages/report/MarketingReportContent.tsx | 6 + .../report/sale/tab/HppPerKandangTab.tsx | 52 ++++---- 4 files changed, 54 insertions(+), 121 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17c2a7b8..4bc65f98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", - "xlsx": "^0.18.5", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -1856,6 +1856,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1925,6 +1926,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2448,6 +2450,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2465,15 +2468,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adler-32": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", - "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2934,19 +2928,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/cfb": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", - "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", - "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "crc-32": "~1.2.0" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2988,15 +2969,6 @@ "node": ">=6" } }, - "node_modules/codepage": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", - "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3067,18 +3039,6 @@ "node": ">=10" } }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3104,7 +3064,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/daisyui": { "version": "5.5.8", @@ -3560,6 +3521,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3733,6 +3695,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4224,15 +4187,6 @@ "react": ">=16.8.0" } }, - "node_modules/frac": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", - "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6220,6 +6174,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6250,6 +6205,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6807,18 +6763,6 @@ "node": ">=0.10.0" } }, - "node_modules/ssf": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", - "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", - "license": "Apache-2.0", - "dependencies": { - "frac": "~1.1.2" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -7148,6 +7092,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7315,6 +7260,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7580,24 +7526,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wmf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", - "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/word": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", - "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7609,19 +7537,10 @@ } }, "node_modules/xlsx": { - "version": "0.18.5", - "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", - "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", "license": "Apache-2.0", - "dependencies": { - "adler-32": "~1.3.0", - "cfb": "~1.2.1", - "codepage": "~1.15.0", - "crc-32": "~1.2.1", - "ssf": "~0.11.2", - "wmf": "~1.0.1", - "word": "~0.3.0" - }, "bin": { "xlsx": "bin/xlsx.njs" }, @@ -7699,4 +7618,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 86b951e5..c6fc9099 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "swr": "^2.3.6", "tailwind-merge": "^3.3.1", "use-debounce": "^10.0.6", - "xlsx": "^0.18.5", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yup": "^1.7.0", "zustand": "^5.0.8" }, @@ -48,4 +48,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx index 160de8b2..d54c935a 100644 --- a/src/components/pages/report/MarketingReportContent.tsx +++ b/src/components/pages/report/MarketingReportContent.tsx @@ -4,6 +4,7 @@ import { JSX, useState } from 'react'; import Tabs from '@/components/Tabs'; import DailyMarketingReportContent from '@/components/pages/report/DailyMarketingReportContent'; +import HppPerKandangTab from './sale/tab/HppPerKandangTab'; type MarketingReportTabType = | 'daily' @@ -21,6 +22,11 @@ const marketingReportTabs: { label: 'Penjualan Harian', content: , }, + { + id: 'daily-hpp', + label: 'HPP Harian Kandang', + content: , + }, ]; const MarketingReportContent = () => { diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 330cfd8f..7d6f0951 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -29,6 +29,7 @@ import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; +import { Icon } from '@iconify/react'; const HppPerKandangTab = () => { // ===== STATE MANAGEMENT ===== @@ -797,28 +798,6 @@ const HppPerKandangTab = () => { } className={{ wrapper: 'w-full', body: 'p-1!' }} > -
- - - - Export - - } - align='end' - > - - - - - -
-
{ />
+
+ + + + Export + + + } + align='end' + > + + + + + +
+
{!isSubmitted ? (
- {!isLoading && - flexRender(cell.column.columnDef.cell, cell.getContext())} + {table.getRowModel().rows.map((row) => { + const customRowContent = renderCustomRow?.(row); - {isLoading &&
} -
+ {!isLoading && + flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + {isLoading &&
} +
+ Rekapitulasi per rentang bobot +
Date: Fri, 19 Dec 2025 16:30:06 +0700 Subject: [PATCH 38/50] refactor(FE-357): Refactor HppPerKandang types and add BaseMetadata --- .gitlab-ci.yml | 2 - src/types/api/report/hpp-per-kandang.d.ts | 47 +++++++++++++++++------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ee8a79a5..935cac46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -165,8 +165,6 @@ deploy:staging: environment: name: staging url: https://stg-lti-erp.mbugroup.id - - # ====== PRODUCTION ====== # build:production: # <<: *build_template diff --git a/src/types/api/report/hpp-per-kandang.d.ts b/src/types/api/report/hpp-per-kandang.d.ts index 2b4522a0..ad3f4e0e 100644 --- a/src/types/api/report/hpp-per-kandang.d.ts +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -1,17 +1,7 @@ +import { BaseMetadata } from '@types/api/base-metadata'; import { Supplier } from '@/types/api/master-data/supplier'; import { Kandang } from '@/types/api/master-data/kandang'; -export type HppPerKandangSummary = { - total_remaining_chicken_birds: number; - total_remaining_chicken_weight_kg: number; - average_weight_kg: number; - total_remaining_value_rp: number; - total_egg_production_pieces: number; - total_egg_production_kg: number; - average_egg_hpp_rp_per_kg: number; - total_egg_value_rp: number; -}; - export type HppPerKandangRow = { id: number; kandang: Kandang; @@ -33,7 +23,40 @@ export type HppPerKandangRow = { remaining_value_rp: number; }; -export type HppPerKandangReport = { +export type HppPerKandangSummaryTotal = { + total_remaining_chicken_birds: number; + total_remaining_chicken_weight_kg: number; + average_weight_kg: number; + total_remaining_value_rp: number; + total_egg_production_pieces: number; + total_egg_production_kg: number; + average_egg_hpp_rp_per_kg: number; + total_egg_value_rp: number; +}; + +export type HppPerKandangPerWeightRange = { + id: number; + weight_range: { + weight_min: number; + weight_max: number; + }; + label: string; + remaining_chicken_birds: number; + remaining_chicken_weight_kg: number; + avg_weight_kg: number; + feed_suppliers: Supplier[]; + doc_suppliers: Supplier[]; + average_doc_price_rp: number; + hpp_rp: number; + remaining_value_rp: number; +}; + +export type HppPerKandangSummary = { + per_weight_range: HppPerKandangPerWeightRange[]; + total: HppPerKandangSummaryTotal; +}; + +export type HppPerKandangReport = BaseMetadata & { period: string; rows: HppPerKandangRow[]; summary: HppPerKandangSummary; From c230c8000bfde80331d4447692356fa2c62a7451 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 08:52:34 +0700 Subject: [PATCH 39/50] refactor(FE-355,356,257): Use API summary per_weight_range for HPP reports --- .../sale/export/HppPerkandangExport.tsx | 226 +++++++----------- .../report/sale/tab/HppPerKandangTab.tsx | 96 ++++++-- 2 files changed, 162 insertions(+), 160 deletions(-) diff --git a/src/components/pages/report/sale/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx index 72e7cbaa..790efef7 100644 --- a/src/components/pages/report/sale/export/HppPerkandangExport.tsx +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -9,7 +9,11 @@ import { Font, pdf, } from '@react-pdf/renderer'; -import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; import { formatDate, formatNumber, formatCurrency } from '@/lib/helper'; Font.register({ @@ -148,74 +152,6 @@ interface HppPerKandangExportParams { }; } -const rekapitulasiData = (data: HppPerKandangReport['rows']) => { - const groups: { [key: string]: HppPerKandangReport['rows'] } = {}; - - data.forEach((item) => { - const key = `${item.weight_range.weight_min}-${item.weight_range.weight_max}`; - if (!groups[key]) { - groups[key] = []; - } - groups[key].push(item); - }); - - return Object.entries(groups) - .map(([key, items]) => ({ - weight_min: items[0].weight_range.weight_min, - weight_max: items[0].weight_range.weight_max, - items, - total_remaining_chicken_birds: items.reduce( - (sum, item) => sum + item.remaining_chicken_birds, - 0 - ), - total_remaining_chicken_weight_kg: items.reduce( - (sum, item) => sum + item.remaining_chicken_weight_kg, - 0 - ), - average_weight_kg: - items.reduce((sum, item) => sum + item.remaining_chicken_weight_kg, 0) / - items.reduce((sum, item) => sum + item.remaining_chicken_birds, 0), - total_hpp_rp: items.reduce((sum, item) => sum + item.hpp_rp, 0), - total_remaining_value_rp: items.reduce( - (sum, item) => sum + item.remaining_value_rp, - 0 - ), - total_egg_production_pieces: items.reduce( - (sum, item) => sum + (item.egg_production_pieces || 0), - 0 - ), - total_egg_production_kg: items.reduce( - (sum, item) => sum + (item.egg_production_kg || 0), - 0 - ), - total_egg_value_rp: items.reduce( - (sum, item) => sum + (item.egg_value_rp || 0), - 0 - ), - average_egg_hpp_rp_per_kg: - items.reduce((sum, item) => sum + (item.egg_hpp_rp_per_kg || 0), 0) / - items.length, - average_doc_price_rp: - items.reduce((sum, item) => sum + item.average_doc_price_rp, 0) / - items.length, - all_feed_suppliers: [ - ...new Set( - items.flatMap( - (item) => item.feed_suppliers?.map((s) => s.alias || s.name) || [] - ) - ), - ], - all_doc_suppliers: [ - ...new Set( - items.flatMap( - (item) => item.doc_suppliers?.map((s) => s.alias || s.name) || [] - ) - ), - ], - })) - .sort((a, b) => a.weight_min - b.weight_min); -}; - const getParameterText = (params: HppPerKandangExportParams['params']) => { const paramsText = []; @@ -260,7 +196,7 @@ const createPDFDocument = ( data: HppPerKandangExportParams['data'], params: HppPerKandangExportParams['params'] ) => { - const rekapitulasiByWeightRange = rekapitulasiData(data.rows); + const rekapitulasiByWeightRange = data.summary?.per_weight_range || []; return ( @@ -328,71 +264,81 @@ const createPDFDocument = ( {/* Table Body - Rekapitulasi */} - {rekapitulasiByWeightRange.map((group, index) => ( - - - - {group.weight_min.toFixed(2)} -{' '} - {group.weight_max.toFixed(2)} - + {rekapitulasiByWeightRange.map( + (group: HppPerKandangPerWeightRange, index: number) => ( + + + {group.label} + + + {formatNumber(group.remaining_chicken_birds)} + + + + {formatNumber(group.remaining_chicken_weight_kg)} + + + + {formatNumber(group.avg_weight_kg)} + + + - + + + - + + + + {group.feed_suppliers + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) + .join(' | ') || '-'} + + + + + {group.doc_suppliers + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) + .join(' | ') || '-'} + + + + {formatCurrency(group.average_doc_price_rp)} + + + - + + + + {formatCurrency( + group.remaining_chicken_birds > 0 + ? group.hpp_rp / group.remaining_chicken_birds + : 0 + )} + + + + - + + + {formatCurrency(group.remaining_value_rp)} + - - - {formatNumber(group.total_remaining_chicken_birds)} - - - - - {formatNumber(group.total_remaining_chicken_weight_kg)} - - - - {formatNumber(group.average_weight_kg)} - - - {formatNumber(group.total_egg_production_pieces)} - - - {formatNumber(group.total_egg_production_kg)} - - - {group.all_feed_suppliers.join(' | ')} - - - {group.all_doc_suppliers.join(' | ')} - - - {formatCurrency(group.average_doc_price_rp)} - - - {formatCurrency(group.total_egg_value_rp)} - - - - {formatCurrency( - group.total_remaining_chicken_birds > 0 - ? group.total_hpp_rp / - group.total_remaining_chicken_birds - : 0 - )} - - - - {formatCurrency(group.average_egg_hpp_rp_per_kg)} - - - {formatCurrency(group.total_remaining_value_rp)} - - - ))} + ) + )} @@ -451,7 +397,7 @@ const createPDFDocument = ( {/* Table Body - Detail Per Kandang */} - {data.rows.map((item, index) => ( + {data.rows.map((item: HppPerKandangRow, index: number) => ( {item.feed_suppliers - ?.map((s) => s.alias || s.name) + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) .join(' | ')} {item.doc_suppliers - ?.map((s) => s.alias || s.name) + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) .join(' | ')} diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index e7081f84..7da3ff36 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -15,7 +15,11 @@ import { SaleReportApi } from '@/services/api/report/marketing-sale'; import Table from '@/components/Table'; import { ColumnDef, Row } from '@tanstack/react-table'; import { formatCurrency, formatNumber } from '@/lib/helper'; -import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; +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'; @@ -225,6 +229,17 @@ const HppPerKandangTab = () => { ? 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 @@ -271,25 +286,49 @@ const HppPerKandangTab = () => { // ===== 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: - data.length > 0 - ? data.reduce((acc, item) => acc + (item.hpp_rp || 0), 0) - : 0, - total_average_doc_price_rp: - data.length > 0 - ? data.reduce( - (acc, item) => acc + (item.average_doc_price_rp || 0), - 0 - ) / data.length - : 0, + total_hpp_rp: 0, + total_average_doc_price_rp: 0, }; - }, [summary]); + }, [summaryTotal, perWeightRangeSummary, data]); const allFeedSuppliers = useMemo(() => { const suppliers = new Set(); - data.forEach((item) => { - item.feed_suppliers?.forEach((s) => { + data.forEach((item: HppPerKandangRow) => { + item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => { suppliers.add(s.alias || s.name); }); }); @@ -298,8 +337,8 @@ const HppPerKandangTab = () => { const allDocSuppliers = useMemo(() => { const suppliers = new Set(); - data.forEach((item) => { - item.doc_suppliers?.forEach((s) => { + data.forEach((item: HppPerKandangRow) => { + item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => { suppliers.add(s.alias || s.name); }); }); @@ -327,7 +366,7 @@ const HppPerKandangTab = () => { const summary = allDataForExport.summary; const excelData: { [key: string]: string | number }[] = allExportData.map( - (item, index) => ({ + (item: HppPerKandangRow, index: number) => ({ No: index + 1, Kandang: item.kandang?.name || '', 'Rentang Bobot': item.weight_range @@ -339,10 +378,13 @@ const HppPerKandangTab = () => { 'Produksi Telur (Butir)': item.egg_production_pieces || 0, 'Produksi Telur (KG)': item.egg_production_kg || 0, 'Feed (Supplier)': - item.feed_suppliers?.map((s) => s.alias || s.name).join(' | ') || - '', + item.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '', 'DOC (Supplier)': - item.doc_suppliers?.map((s) => s.alias || s.name).join(' | ') || '', + 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, @@ -588,7 +630,11 @@ const HppPerKandangTab = () => { accessorKey: 'feed_suppliers', cell: (props) => { const suppliers = props.row.original.feed_suppliers; - return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-'; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); }, footer: () => (
@@ -602,7 +648,11 @@ const HppPerKandangTab = () => { accessorKey: 'doc_suppliers', cell: (props) => { const suppliers = props.row.original.doc_suppliers; - return suppliers?.map((s) => s.alias || s.name).join(' | ') || '-'; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); }, footer: () => (
From c8effe447364448115fc603216dfed5c7254c4c2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 09:17:31 +0700 Subject: [PATCH 40/50] feat(FE-355,357): Render per-weight-range summary rows --- .../report/sale/tab/HppPerKandangTab.tsx | 73 ++++++++++++++++++- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 7da3ff36..f7e92e71 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -29,6 +29,7 @@ import Menu from '@/components/menu/Menu'; import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; import toast from 'react-hot-toast'; import * as XLSX from 'xlsx'; +import { Supplier } from '@/types/api/master-data/supplier'; interface Totals { total_hpp_rp: number; @@ -738,7 +739,7 @@ const HppPerKandangTab = () => { const renderCustomRow = useCallback( (row: Row) => { if (row.index === data.length - 1) { - return ( + const rows = [
+ + + {perWeightRangeSummary.map( + (item: HppPerKandangPerWeightRange, index = 0) => ( + + + + + + + + + + + + + + ) + )} + +
+ {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: Supplier) => s.alias || s.name) + .join(' | ') || '-'} + + {item.doc_suppliers + ?.map((s: Supplier) => s.alias || s.name) + .join(' | ') || '-'} + + {formatCurrency(item.average_doc_price_rp)} + + {formatCurrency(item.hpp_rp)} + + {formatCurrency(item.remaining_value_rp)} +
+
- {index + 1} - - ALL - - {item.label} - + {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: Supplier) => s.alias || s.name) .join(' | ') || '-'} + {item.doc_suppliers ?.map((s: Supplier) => s.alias || s.name) .join(' | ') || '-'} + {formatCurrency(item.average_doc_price_rp)} + {formatCurrency(item.hpp_rp)} + - {formatCurrency(item.remaining_value_rp)}
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
{ ); } - return rows; + return [defaultRow, ...customRows]; } return null; From eb8a1567c68dc7439f7499c1eb533088cfdcdeb6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 20 Dec 2025 09:47:50 +0700 Subject: [PATCH 43/50] refactor(FE-355): Render weight-range summaries as table rows --- .../report/sale/tab/HppPerKandangTab.tsx | 100 ++++++++---------- 1 file changed, 44 insertions(+), 56 deletions(-) diff --git a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx index 5248521f..1748a6c7 100644 --- a/src/components/pages/report/sale/tab/HppPerKandangTab.tsx +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -747,7 +747,7 @@ const HppPerKandangTab = () => { {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())}
- - - {perWeightRangeSummary.map( - (item: HppPerKandangPerWeightRange, index = 0) => ( - - - - - - - - - - - - - - - - - ) - )} - -
{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: Supplier) => s.alias || s.name) - .join(' | ') || '-'} - - {item.doc_suppliers - ?.map((s: Supplier) => s.alias || s.name) - .join(' | ') || '-'} - - {formatCurrency(item.average_doc_price_rp)} - - {formatCurrency(item.hpp_rp)} - - - {formatCurrency(item.remaining_value_rp)} -
-
{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)} +
{formatNumber(item.remaining_chicken_weight_kg)} -- + {formatNumber(item.egg_production_pieces)} + + {formatNumber(item.egg_production_kg)} + {item.feed_suppliers ?.map((s) => s.alias || s.name) @@ -807,9 +766,13 @@ const HppPerKandangTab = () => { {formatCurrency(item.average_doc_price_rp)} - + {formatCurrency(item.egg_value_rp)} + {formatCurrency(item.hpp_rp)}- + {formatCurrency(item.egg_hpp_rp_per_kg)} + {formatCurrency(item.remaining_value_rp)}