diff --git a/package-lock.json b/package-lock.json index 85845443..56433eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,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" }, @@ -1871,6 +1871,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" } @@ -1947,6 +1948,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", @@ -2470,6 +2472,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2487,15 +2490,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", @@ -2966,19 +2960,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", @@ -3020,15 +3001,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", @@ -3111,18 +3083,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", @@ -3158,7 +3118,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", @@ -3624,6 +3585,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3797,6 +3759,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4311,15 +4274,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", @@ -6370,6 +6324,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" } @@ -6400,6 +6355,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" }, @@ -6974,18 +6930,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", @@ -7345,6 +7289,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7512,6 +7457,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7787,24 +7733,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", @@ -7816,19 +7744,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" }, @@ -7906,4 +7825,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a2db399a..039101dc 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,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" }, @@ -50,4 +50,4 @@ "tailwindcss": "^4", "typescript": "^5" } -} \ No newline at end of file +} 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/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 0d5b9bc8..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'; 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/SaleReportTabs.tsx b/src/components/pages/report/sale/SaleReportTabs.tsx new file mode 100644 index 00000000..988c16b2 --- /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/export/HppPerkandangExport.tsx b/src/components/pages/report/sale/export/HppPerkandangExport.tsx new file mode 100644 index 00000000..0a712a6c --- /dev/null +++ b/src/components/pages/report/sale/export/HppPerkandangExport.tsx @@ -0,0 +1,497 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; +import { formatDate, formatNumber, formatCurrency } 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; + }; +} + +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}`); + + return paramsText; +}; + +const createPDFDocument = ( + data: HppPerKandangExportParams['data'], + params: HppPerKandangExportParams['params'] +) => { + const rekapitulasiByWeightRange = data.summary?.per_weight_range || []; + + 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) + + + Produksi Telur (Butir) + + + Produksi Telur (Kg) + + + Feed (Supplier) + + + DOC (Supplier) + + + Rata-Rata Harga DOC + + + Nilai Nominal Telur + + + HPP Ayam + + + HPP Telur (RP/KG) + + + Nominal Sisa + + + + {/* Table Body - Rekapitulasi */} + {rekapitulasiByWeightRange.map( + (group: HppPerKandangPerWeightRange, index: number) => ( + + + {group.label} + + + {formatNumber(group.remaining_chicken_birds)} + + + + {formatNumber(group.remaining_chicken_weight_kg)} + + + + {formatNumber(group.avg_weight_kg)} + + + {formatNumber(group.egg_production_pieces)} + + + {formatNumber(group.egg_production_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.egg_value_rp)} + + + {formatCurrency(group.hpp_rp)} + + + {formatCurrency(group.egg_hpp_rp_per_kg)} + + + {formatCurrency(group.remaining_value_rp)} + + + ) + )} + + + + {/* Detail Per Kandang Section */} + + Detail Per Kandang + + + {/* Table Header */} + + + No + + + Kandang + + + Rentang BW + + + Rata-Rata Bobot (Kg) + + + Sisa Ekor + + + Sisa Kg (Ayam) + + + Produksi Telur (Butir) + + + Produksi Telur (Kg) + + + Feed (Supplier) + + + DOC (Supplier) + + + Rata-Rata Harga DOC + + + Nilai Nominal Telur + + + HPP Ayam + + + HPP Telur (RP/KG) + + + Nominal Sisa + + + + {/* Table Body - Detail Per Kandang */} + {data.rows.map((item: HppPerKandangRow, index: number) => ( + + + {index + 1} + + + {item.kandang?.name || '-'} + + + + {item.weight_range.weight_min.toFixed(2)} -{' '} + {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.egg_production_pieces)} + + + {formatNumber(item.egg_production_kg)} + + + + {item.feed_suppliers + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) + .join(' | ')} + + + + + {item.doc_suppliers + ?.map( + (s: { alias?: string; name: string }) => + s.alias || s.name + ) + .join(' | ')} + + + + {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)} + + + ))} + + + + + ); +}; + +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; + + 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); + 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 new file mode 100644 index 00000000..7d6f0951 --- /dev/null +++ b/src/components/pages/report/sale/tab/HppPerKandangTab.tsx @@ -0,0 +1,959 @@ +import { useState, useMemo, useCallback } from 'react'; +import { ChangeEventHandler } from 'react'; +import useSWR from 'swr'; +import Card from '@/components/Card'; +import SelectInput, { + useSelect, + OptionType, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import NumberInput from '@/components/input/NumberInput'; +import { AreaApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { KandangApi } from '@/services/api/master-data'; +import { SaleReportApi } from '@/services/api/report/marketing-sale'; +import Table from '@/components/Table'; +import { ColumnDef, Row, flexRender } from '@tanstack/react-table'; +import { formatCurrency, formatNumber } from '@/lib/helper'; +import { + HppPerKandangReport, + HppPerKandangRow, + HppPerKandangPerWeightRange, +} from '@/types/api/report/hpp-per-kandang'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import Button from '@/components/Button'; +import Dropdown from '@/components/Dropdown'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import { generateHppPerKandangPDF } from '../export/HppPerkandangExport'; +import toast from 'react-hot-toast'; +import * as XLSX from 'xlsx'; +import { Icon } from '@iconify/react'; + +const HppPerKandangTab = () => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + + // ===== TABLE FILTER STATE ===== + const { state: tableFilterState, updateFilter } = useTableFilter({ + initial: { + area_id: [] as string[], + location_id: [] as string[], + kandang_id: [] as string[], + weight_min: '', + weight_max: '', + period: '', + sort_by: '', + show_unrecorded: false, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: locationOptions, isLoadingOptions: isLoadingLocations } = + useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = + useSelect(KandangApi.basePath, 'id', 'name', 'search'); + + const showUnrecordedOptions: OptionType[] = [ + { value: 'false', label: 'Sembunyikan' }, + { value: 'true', label: 'Tampilkan' }, + ]; + + const areaChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'area_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const locationChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'location_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const kandangChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'kandang_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const weightMinChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('weight_min', val ? String(parseFloat(val) || 0) : ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const weightMaxChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('weight_max', val ? String(parseFloat(val) || 0) : ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const periodChangeHandler = useCallback>( + (e) => { + const val = e.target.value; + updateFilter('period', val || ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const showUnrecordedChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + updateFilter('show_unrecorded', newVal?.value === 'true'); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const resetFilters = useCallback(() => { + updateFilter('area_id', []); + updateFilter('location_id', []); + updateFilter('kandang_id', []); + updateFilter('weight_min', ''); + updateFilter('weight_max', ''); + updateFilter('period', ''); + updateFilter('sort_by', ''); + updateFilter('show_unrecorded', false); + setIsSubmitted(false); + }, [updateFilter]); + + const handleSubmit = useCallback(() => { + if (!tableFilterState.period) { + toast.error('Periode wajib diisi'); + return; + } + setIsSubmitted(true); + }, [tableFilterState.period]); + + // ===== DATA FETCHING ===== + const { data: hppPerKandang, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + location_id: + tableFilterState.location_id.length > 0 + ? tableFilterState.location_id.join(',') + : undefined, + kandang_id: + tableFilterState.kandang_id.length > 0 + ? tableFilterState.kandang_id.join(',') + : undefined, + weight_min: tableFilterState.weight_min || undefined, + weight_max: tableFilterState.weight_max || undefined, + period: tableFilterState.period || undefined, + sort_by: tableFilterState.sort_by || undefined, + show_unrecorded: tableFilterState.show_unrecorded, + }; + + return ['hpp-per-kandang-report', params]; + } + : null, + ([, params]) => + SaleReportApi.getHppPerKandangReport( + params.area_id, + params.location_id, + params.kandang_id, + params.weight_min, + params.weight_max, + params.period, + params.sort_by, + params.show_unrecorded + ) + ); + + const data: HppPerKandangReport['rows'] = useMemo( + () => + isResponseSuccess(hppPerKandang) + ? (hppPerKandang?.data?.rows as HppPerKandangReport['rows']) || [] + : [], + [hppPerKandang] + ); + + const summaryTotal = + isResponseSuccess(hppPerKandang) && hppPerKandang?.data?.summary?.total + ? hppPerKandang.data.summary.total + : undefined; + + 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 + ? hppPerKandang.data.period + : undefined; + + // ===== EXPORT DATA FETCHER ===== + const hppPerKandangExport = + useCallback(async (): Promise => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + location_id: + tableFilterState.location_id.length > 0 + ? tableFilterState.location_id.join(',') + : undefined, + kandang_id: + tableFilterState.kandang_id.length > 0 + ? tableFilterState.kandang_id.join(',') + : undefined, + weight_min: tableFilterState.weight_min || undefined, + weight_max: tableFilterState.weight_max || undefined, + period: tableFilterState.period || undefined, + sort_by: tableFilterState.sort_by || undefined, + show_unrecorded: tableFilterState.show_unrecorded, + limit: 10000, + page: 1, + }; + + const response = await SaleReportApi.getHppPerKandangReport( + params.area_id, + params.location_id, + params.kandang_id, + params.weight_min, + params.weight_max, + params.period, + params.sort_by, + params.show_unrecorded + ); + + return isResponseSuccess(response) ? response.data : null; + }, [tableFilterState]); + + // ===== TABLE COLUMNS DEFINITION ===== + const allFeedSuppliers = useMemo(() => { + const suppliers = new Set(); + data.forEach((item: HppPerKandangRow) => { + item.feed_suppliers?.forEach((s: { alias?: string; name: string }) => { + suppliers.add(s.alias || s.name); + }); + }); + return Array.from(suppliers).join(' | '); + }, [data]); + + const allDocSuppliers = useMemo(() => { + const suppliers = new Set(); + data.forEach((item: HppPerKandangRow) => { + item.doc_suppliers?.forEach((s: { alias?: string; name: string }) => { + suppliers.add(s.alias || s.name); + }); + }); + return Array.from(suppliers).join(' | '); + }, [data]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await hppPerKandangExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const allExportData = + allDataForExport.rows as HppPerKandangReport['rows']; + + const summaryTotal = allDataForExport.summary.total; + + const excelData: { [key: string]: string | number }[] = allExportData.map( + (item: HppPerKandangRow, index: number) => ({ + No: index + 1, + Kandang: item.kandang?.name || '', + 'Rentang Bobot': item.weight_range + ? `${formatNumber(item.weight_range.weight_min)} - ${formatNumber(item.weight_range.weight_max)}` + : '', + 'Rata-Rata Bobot (KG)': item.avg_weight_kg || 0, + 'Sisa Ayam (Ekor)': item.remaining_chicken_birds || 0, + 'Sisa Ayam (KG)': item.remaining_chicken_weight_kg || 0, + 'Produksi Telur (Butir)': item.egg_production_pieces || 0, + 'Produksi Telur (KG)': item.egg_production_kg || 0, + 'Feed (Supplier)': + item.feed_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '', + 'DOC (Supplier)': + item.doc_suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '', + 'Rata-Rata Harga DOC (RP)': item.average_doc_price_rp || 0, + 'Nilai Nominal Telur (RP)': item.egg_value_rp || 0, + 'HPP Ayam (RP)': item.hpp_rp || 0, + 'HPP Telur (RP/KG)': item.egg_hpp_rp_per_kg || 0, + 'Nilai Nominal Sisa Ayam (RP)': item.remaining_value_rp || 0, + }) + ); + + excelData.push({ + No: 'TOTAL', + Kandang: 'ALL', + 'Rentang Bobot': '-', + 'Rata-Rata Bobot (KG)': 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)': + 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); + + const colWidths = [ + { wch: 5 }, // No + { wch: 30 }, // Kandang + { wch: 15 }, // Rentang Bobot + { wch: 18 }, // Rata-Rata Bobot (KG) + { wch: 15 }, // Sisa Ayam (Ekor) + { wch: 15 }, // Sisa Ayam (KG) + { wch: 18 }, // Produksi Telur (Butir) + { wch: 18 }, // Produksi Telur (KG) + { wch: 20 }, // Feed (Supplier) + { wch: 20 }, // DOC (Supplier) + { wch: 20 }, // Rata-Rata Harga DOC (RP) + { wch: 20 }, // Nilai Nominal Telur (RP) + { wch: 15 }, // HPP Ayam (RP) + { wch: 18 }, // HPP Telur (RP/KG) + { wch: 25 }, // Nilai Nominal Sisa Ayam (RP) + ]; + worksheet['!cols'] = colWidths; + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, 'HPP Per Kandang'); + + const filename = `laporan-hpp-harian-kandang-periode-${tableFilterState.period}.xlsx`; + + XLSX.writeFile(workbook, filename); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [ + hppPerKandangExport, + tableFilterState, + areaOptions, + locationOptions, + kandangOptions, + ]); + + const handleExportPDF = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await hppPerKandangExport(); + + if ( + !allDataForExport || + !allDataForExport?.rows || + allDataForExport.rows.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const areaName = + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id + .map( + (id) => + areaOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Area' + : 'Semua Area'; + + const locationName = + tableFilterState.location_id.length > 0 + ? tableFilterState.location_id + .map( + (id) => + locationOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Lokasi' + : 'Semua Lokasi'; + + const kandangName = + tableFilterState.kandang_id.length > 0 + ? tableFilterState.kandang_id + .map( + (id) => + kandangOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Kandang' + : 'Semua Kandang'; + + await generateHppPerKandangPDF(allDataForExport, { + area_name: areaName, + location_name: locationName, + kandang_name: kandangName, + period: tableFilterState.period, + weight_min: tableFilterState.weight_min, + weight_max: tableFilterState.weight_max, + show_unrecorded: tableFilterState.show_unrecorded.toString(), + sort_by: tableFilterState.sort_by, + }); + + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + hppPerKandangExport, + tableFilterState, + areaOptions, + locationOptions, + kandangOptions, + ]); + + const getTableColumns = (): ColumnDef[] => { + const tableColumns: ColumnDef[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
TOTAL
, + }, + { + id: 'kandang_name', + header: 'Kandang', + accessorKey: 'kandang.name', + cell: (props) => { + const kandang = props.row.original.kandang; + return kandang?.name || '-'; + }, + footer: () =>
ALL
, + }, + { + id: 'weight_range', + header: 'Rentang Bobot', + accessorKey: 'weight_range', + cell: (props) => { + const weightRange = props.row.original.weight_range; + return weightRange + ? `${formatNumber(weightRange.weight_min)} - ${formatNumber(weightRange.weight_max)}` + : '-'; + }, + footer: () =>
-
, + }, + { + id: 'avg_weight_kg', + header: 'Rata-Rata Bobot (KG)', + accessorKey: 'avg_weight_kg', + cell: (props) => { + const value = props.row.original.avg_weight_kg; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summaryTotal?.average_weight_kg || 0)} +
+ ), + }, + { + id: 'remaining_chicken_birds', + header: 'Sisa Ayam (Ekor)', + accessorKey: 'remaining_chicken_birds', + cell: (props) => { + const value = props.row.original.remaining_chicken_birds; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)} +
+ ), + }, + { + id: 'remaining_chicken_weight_kg', + header: 'Sisa Ayam (KG)', + accessorKey: 'remaining_chicken_weight_kg', + cell: (props) => { + const value = props.row.original.remaining_chicken_weight_kg; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)} +
+ ), + }, + { + id: 'egg_production_pieces', + header: 'Produksi Telur (Butir)', + accessorKey: 'egg_production_pieces', + cell: (props) => { + const value = props.row.original.egg_production_pieces; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summaryTotal?.total_egg_production_pieces || 0)} +
+ ), + }, + { + id: 'egg_production_kg', + header: 'Produksi Telur (KG)', + accessorKey: 'egg_production_kg', + cell: (props) => { + const value = props.row.original.egg_production_kg; + return
{formatNumber(value)}
; + }, + footer: () => ( +
+ {formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)} +
+ ), + }, + { + id: 'feed_suppliers', + header: 'Feed (Supplier)', + accessorKey: 'feed_suppliers', + cell: (props) => { + const suppliers = props.row.original.feed_suppliers; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); + }, + footer: () => ( +
+ {allFeedSuppliers || '-'} +
+ ), + }, + { + id: 'doc_suppliers', + header: 'DOC (Supplier)', + accessorKey: 'doc_suppliers', + cell: (props) => { + const suppliers = props.row.original.doc_suppliers; + return ( + suppliers + ?.map((s: { alias?: string; name: string }) => s.alias || s.name) + .join(' | ') || '-' + ); + }, + footer: () => ( +
+ {allDocSuppliers || '-'} +
+ ), + }, + { + id: 'average_doc_price_rp', + header: 'Rata-Rata Harga DOC (RP)', + accessorKey: 'average_doc_price_rp', + cell: (props) => { + const value = props.row.original.average_doc_price_rp; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)} +
+ ), + }, + { + id: 'egg_value_rp', + header: 'Nilai Nominal Telur (RP)', + accessorKey: 'egg_value_rp', + cell: (props) => { + const value = props.row.original.egg_value_rp; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summaryTotal?.total_egg_value_rp || 0)} +
+ ), + }, + { + id: 'hpp_rp', + header: 'HPP Ayam (RP)', + accessorKey: 'hpp_rp', + cell: (props) => { + const value = props.row.original.hpp_rp; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summaryTotal?.total_hpp_rp || 0)} +
+ ), + }, + { + id: 'egg_hpp_rp_per_kg', + header: 'HPP Telur (RP/KG)', + accessorKey: 'egg_hpp_rp_per_kg', + cell: (props) => { + const value = props.row.original.egg_hpp_rp_per_kg; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)} +
+ ), + }, + { + id: 'remaining_value_rp', + header: 'Nilai Nominal Sisa Ayam (RP)', + accessorKey: 'remaining_value_rp', + cell: (props) => { + const value = props.row.original.remaining_value_rp; + return
{formatCurrency(value)}
; + }, + footer: () => ( +
+ {formatCurrency(summaryTotal?.total_remaining_value_rp || 0)} +
+ ), + }, + ]; + return tableColumns; + }; + + // ===== CUSTOM ROW RENDERER ===== + const renderCustomRow = useCallback( + (row: Row) => { + if (row.index === data.length - 1) { + const defaultRow = ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ); + + const customRows = [ + + + Rekapitulasi per rentang bobot + + , + ]; + + if (perWeightRangeSummary.length > 0) { + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index = 0) => { + customRows.push( + + {index + 1} + ALL + {item.label} + + {formatNumber(item.avg_weight_kg)} + + + {formatNumber(item.remaining_chicken_birds)} + + + {formatNumber(item.remaining_chicken_weight_kg)} + + + {formatNumber(item.egg_production_pieces)} + + + {formatNumber(item.egg_production_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.egg_value_rp)} + + {formatCurrency(item.hpp_rp)} + + {formatCurrency(item.egg_hpp_rp_per_kg)} + + + {formatCurrency(item.remaining_value_rp)} + + + ); + } + ); + } + + return [defaultRow, ...customRows]; + } + + return null; + }, + [data, perWeightRangeSummary] + ); + + return ( +
+ HPP Harian Kandang (${period})` + : 'Laporan > HPP Harian Kandang' + } + className={{ wrapper: 'w-full', body: 'p-1!' }} + > +
+ + (tableFilterState.area_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={areaChangeHandler} + isLoading={isLoadingAreas} + isClearable + /> + + (tableFilterState.location_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={locationChangeHandler} + isLoading={isLoadingLocations} + isClearable + /> + + (tableFilterState.kandang_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={kandangChangeHandler} + isLoading={isLoadingKandangs} + isClearable + /> +
+ +
+
+ + +
+ + opt.value === 'true') || + null + : showUnrecordedOptions.find((opt) => opt.value === 'false') || + null + } + onChange={showUnrecordedChangeHandler} + /> +
+ +
+ + + + Export + + + } + align='end' + > + + + + + +
+ +
+ + {!isSubmitted ? ( +
+ Silakan pilih filter dan klik tombol Cari untuk menampilkan data. +
+ ) : isLoading ? ( +
+ +
+ ) : data.length === 0 ? ( +
+ Tidak ada data yang dapat ditampilkan... +
+ ) : ( + 0} + renderCustomRow={renderCustomRow} + className={{ + containerClassName: 'w-full mt-6', + tableWrapperClassName: 'overflow-x-auto mt-4', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-700 text-left border border-gray-200', + bodyRowClassName: + 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', + bodyColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: + 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', + }} + /> + )} + + + ); +}; + +export default HppPerKandangTab; diff --git a/src/services/api/report/marketing-sale.ts b/src/services/api/report/marketing-sale.ts new file mode 100644 index 00000000..bb9c1f49 --- /dev/null +++ b/src/services/api/report/marketing-sale.ts @@ -0,0 +1,53 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { HppPerKandangReport } from '@/types/api/report/hpp-per-kandang'; + +export class MarketingSaleReportService extends BaseApiService< + HppPerKandangReport, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getHppPerKandangReport( + area_id?: string, + location_id?: string, + kandang_id?: string, + weight_min?: string, + weight_max?: string, + 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, + }, + } + ); + } +} + +export const SaleReportApi = new MarketingSaleReportService( + 'reports/marketings' +); + +// export const SaleReportApi = new MarketingSaleReportService( +// '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 new file mode 100644 index 00000000..824a3837 --- /dev/null +++ b/src/types/api/report/hpp-per-kandang.d.ts @@ -0,0 +1,69 @@ +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 HppPerKandangRow = { + 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 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; + total_hpp_rp: number; + total_average_doc_price_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; + 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 HppPerKandangSummary = { + per_weight_range: HppPerKandangPerWeightRange[]; + total: HppPerKandangSummaryTotal; +}; + +export type HppPerKandangReport = BaseMetadata & { + period: string; + rows: HppPerKandangRow[]; + summary: HppPerKandangSummary; +};