diff --git a/package-lock.json b/package-lock.json index 16307a12..c29a16a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" }, @@ -3135,7 +3138,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", @@ -3601,6 +3605,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3774,6 +3779,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5238,6 +5244,7 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz", "integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -6338,6 +6345,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" } @@ -6368,6 +6376,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" }, @@ -7301,6 +7310,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7468,6 +7478,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx index 1b4ebc45..62f3fa20 100644 --- a/src/app/closing/detail/page.tsx +++ b/src/app/closing/detail/page.tsx @@ -24,6 +24,11 @@ const ClosingDetailPage = () => { () => ClosingApi.getPenjualan(Number(closingId)) ); + const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR( + closingId ? `hpp-ekspedisi-${closingId}` : null, + () => ClosingApi.getHppEkspedisi(Number(closingId)) + ); + if (!closingId) { router.back(); @@ -39,7 +44,7 @@ const ClosingDetailPage = () => { return; } - const isLoading = isLoadingClosing || isLoadingSales; + const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi; return (
@@ -50,6 +55,11 @@ const ClosingDetailPage = () => { id={Number(closingId)} initialValue={closing.data} salesData={isResponseSuccess(salesData) ? salesData.data : undefined} + hppExpeditionData={ + isResponseSuccess(hppEkspedisiData) + ? hppEkspedisiData.data + : undefined + } /> )}
diff --git a/src/app/report/logistic-stock/layout.tsx b/src/app/report/logistic-stock/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/report/logistic-stock/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/logistic-stock/page.tsx b/src/app/report/logistic-stock/page.tsx new file mode 100644 index 00000000..77ba31ed --- /dev/null +++ b/src/app/report/logistic-stock/page.tsx @@ -0,0 +1,7 @@ +import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs'; + +const LogisticStock = () => { + return ; +}; + +export default LogisticStock; 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..52a3d4dd --- /dev/null +++ b/src/app/report/marketing/page.tsx @@ -0,0 +1,11 @@ +import MarketingReportContent from '@/components/pages/report/MarketingReportContent'; + +const MarketingReportPage = () => { + return ( +
+ +
+ ); +}; + +export default MarketingReportPage; 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/FloatingActionsButton.tsx b/src/components/FloatingActionsButton.tsx index 2e4eed07..974ca280 100644 --- a/src/components/FloatingActionsButton.tsx +++ b/src/components/FloatingActionsButton.tsx @@ -5,6 +5,8 @@ import Tooltip from '@/components/Tooltip'; import { cn } from '@/lib/helper'; import { Icon } from '@iconify/react'; +import { useAuth } from '@/services/hooks/useAuth'; + type FloatingActionsButtonProps = { actions: { action: 'DETAIL' | 'EDIT' | 'DELETE'; @@ -13,6 +15,7 @@ type FloatingActionsButtonProps = { onClick?: () => void; hidden?: boolean; disabled?: boolean; + permissions?: string | string[]; }[]; approvals: { action: 'APPROVED' | 'REJECTED'; @@ -20,6 +23,7 @@ type FloatingActionsButtonProps = { label?: string; onClick?: () => void; disabled?: boolean; + permissions?: string | string[]; }[]; selectedRowIds: number[]; onClose: () => void; @@ -31,6 +35,7 @@ const FloatingActionsButton = ({ selectedRowIds, onClose, }: FloatingActionsButtonProps) => { + const { permissionCheck } = useAuth(); // Jika tidak ada baris yang dipilih, jangan tampilkan FAB const positionStyles = selectedRowIds.length > 0 @@ -71,7 +76,18 @@ const FloatingActionsButton = ({
{/* Render Aksi dari props.actions */} {actions - .filter((action) => !action.hidden) + .filter((action) => { + if (action.hidden) return false; + if (action.permissions) { + if (typeof action.permissions === 'string') { + return permissionCheck(action.permissions); + } + return action.permissions.some((permission) => + permissionCheck(permission) + ); + } + return true; + }) .map((action, index) => { return ( - ))} + {approvals + .filter((approval) => { + if (approval.permissions) { + if (typeof approval.permissions === 'string') { + return permissionCheck(approval.permissions); + } + return approval.permissions.some((permission) => + permissionCheck(permission) + ); + } + return true; + }) + .map((approval, index) => ( + + ))}
diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 3a09c0b1..fc8cbb18 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -9,10 +9,13 @@ import Drawer from '@/components/Drawer'; import Navbar from '@/components/Navbar'; import Button from '@/components/Button'; import SidebarMenu from '@/components/molecules/SidebarMenu'; +import PermissionNotFound from '@/components/helper/PermissionNotFound'; import { useUiStore } from '@/stores/ui/ui.store'; import { MAIN_DRAWER_LINKS } from '@/config/constant'; import { isPathActive } from '@/lib/helper'; +import { ROUTE_PERMISSIONS } from '@/config/route-permission'; +import { useAuth } from '@/services/hooks/useAuth'; const MainDrawerContent = () => { const pathname = usePathname(); @@ -62,6 +65,11 @@ const MainDrawer = ({ }>) => { const { mainDrawerOpen, setMainDrawerOpen } = useUiStore(); const pathname = usePathname(); + const { permissionCheck } = useAuth(); + + const isPermitted = ROUTE_PERMISSIONS[pathname]?.some((permission) => + permissionCheck(permission) + ); const getPageTitle = useCallback(() => { let title = ''; @@ -101,6 +109,10 @@ const MainDrawer = ({ setMainDrawerOpen(!mainDrawerOpen); }; + if (!isPermitted) { + return ; + } + return ( {
@@ -67,7 +67,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => { content: 'w-52 mt-3', }} > - + diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 2ad2477d..8f685452 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -21,6 +21,7 @@ export interface TabsProps className?: | string | { + container?: string; wrapper?: string; tab?: string; content?: string; @@ -53,10 +54,14 @@ const Tabs = ({ onTabChange?.(tabId); }; - const { wrapper: wrapperClassName, tab: tabClassName } = - typeof className === 'object' - ? className - : { wrapper: className, tab: undefined }; + const { + container: containerClassName, + wrapper: wrapperClassName, + tab: tabClassName, + content: contentClassName, + } = typeof className === 'object' + ? className + : { wrapper: className, tab: undefined }; const getTabsClasses = () => { const variantClasses: Record = { @@ -104,7 +109,7 @@ const Tabs = ({ {...props} className={cn( 'w-full', - typeof className === 'string' ? className : undefined + typeof className === 'string' ? className : containerClassName )} >
@@ -121,7 +126,9 @@ const Tabs = ({ ))}
- {activeContent &&
{activeContent}
} + {activeContent && ( +
{activeContent}
+ )}
); }; diff --git a/src/components/helper/PermissionNotFound.tsx b/src/components/helper/PermissionNotFound.tsx new file mode 100644 index 00000000..75e48c62 --- /dev/null +++ b/src/components/helper/PermissionNotFound.tsx @@ -0,0 +1,12 @@ +const PermissionNotFound = () => { + return ( +
+

Permission Not Found

+

+ You do not have permission to access this page. +

+
+ ); +}; + +export default PermissionNotFound; diff --git a/src/components/helper/RequirePermission.tsx b/src/components/helper/RequirePermission.tsx new file mode 100644 index 00000000..2a7061ed --- /dev/null +++ b/src/components/helper/RequirePermission.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { useAuth } from '@/services/hooks/useAuth'; + +interface RequirePermissionProps { + children: React.ReactNode; + permissions: string | string[]; +} + +const RequirePermission = ({ + children, + permissions, +}: RequirePermissionProps) => { + const { permissionCheck } = useAuth(); + + const isPermitted = + typeof permissions === 'string' + ? permissionCheck(permissions) + : permissions.some((permission) => permissionCheck(permission)); + + if (!isPermitted) { + return null; + } + + return <>{children}; +}; + +export default RequirePermission; diff --git a/src/components/menu/MenuItem.tsx b/src/components/menu/MenuItem.tsx index dce81dac..61af4b04 100644 --- a/src/components/menu/MenuItem.tsx +++ b/src/components/menu/MenuItem.tsx @@ -8,6 +8,7 @@ interface MenuItemProps { href?: string; icon?: string; active?: boolean; + isLoading?: boolean; onClick?: () => void; className?: string; } @@ -17,6 +18,7 @@ const MenuItem = ({ href, icon, active = false, + isLoading = false, className, onClick, }: MenuItemProps) => { @@ -50,17 +52,28 @@ const MenuItem = ({ return (
  • - {href && ( + {!isLoading && href && ( {menuItemContent} )} - {!href && ( + {!isLoading && !href && ( )} + + {isLoading && ( + + )}
  • ); }; diff --git a/src/components/molecules/SidebarMenu.tsx b/src/components/molecules/SidebarMenu.tsx index 6a217dcc..4b85c2c8 100644 --- a/src/components/molecules/SidebarMenu.tsx +++ b/src/components/molecules/SidebarMenu.tsx @@ -2,6 +2,7 @@ import Link from 'next/link'; import Menu from '@/components/menu/Menu'; import { Icon } from '@iconify/react'; import { cn, isPathActive } from '@/lib/helper'; +import { useAuth } from '@/services/hooks/useAuth'; export interface SidebarMenuItem { type?: 'item' | 'title'; @@ -9,6 +10,7 @@ export interface SidebarMenuItem { link: string; icon?: string; submenu?: SidebarMenuItem[]; + permission?: string[]; } interface SidebarMenuItemProps { @@ -22,8 +24,17 @@ interface SidebarMenuProps { } const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => { + const { permissionCheck } = useAuth(); const isItemActive = isPathActive(activeLink, item.link); + const isUserPermitted = item.permission + ? item.permission?.some((permissionName) => permissionCheck(permissionName)) + : true; + + if (!isUserPermitted) { + return null; + } + const menuItemWithoutSubmenu = (
  • { const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => { return ( - {menu.map((menuItem, menuIdx) => ( - - ))} + {menu.map((menuItem, menuIdx) => { + return ( + + ); + })} ); }; diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx index e12769a7..94647f87 100644 --- a/src/components/pages/closing/ClosingDetail.tsx +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -6,27 +6,32 @@ import { Icon } from '@iconify/react'; import Button from '@/components/Button'; import Tabs from '@/components/Tabs'; import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; +import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent'; +import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent'; import { ClosingGeneralInformation, BaseClosingSales, + ClosingHppExpedition, } from '@/types/api/closing'; -import ClosingSapronakTabContent from './ClosingSapronakTabContent'; import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent'; -import SalesReportTable from './sale/SalesReportTable'; import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent'; +import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; +import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable'; interface ClosingDetailProps { id: number; initialValue?: ClosingGeneralInformation; salesData?: BaseClosingSales; + hppExpeditionData?: ClosingHppExpedition; } const ClosingDetail: React.FC = ({ id, initialValue, salesData, + hppExpeditionData, }) => { const [activeTab, setActiveTab] = useState('sapronak'); @@ -55,12 +60,12 @@ const ClosingDetail: React.FC = ({ { id: 'hppEkspedisi', label: 'HPP Ekspedisi', - content: 'HPP Ekspedisi', + content: , }, { id: 'dataProduksi', label: 'Data Produksi', - content: 'Data Produksi', + content: , }, { id: 'keuangan', diff --git a/src/components/pages/closing/ClosingProductionDataTabContent.tsx b/src/components/pages/closing/ClosingProductionDataTabContent.tsx new file mode 100644 index 00000000..bffe1707 --- /dev/null +++ b/src/components/pages/closing/ClosingProductionDataTabContent.tsx @@ -0,0 +1,235 @@ +'use client'; + +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { formatNumber } from '@/lib/helper'; + +interface ClosingProductionDataTabContentProps { + projectFlockId: number; +} + +const ClosingProductionDataTabContent = ({ + projectFlockId, +}: ClosingProductionDataTabContentProps) => { + const { data: productionData, isLoading } = useSWR( + `${ClosingApi.basePath}/${projectFlockId}/production-data`, + () => ClosingApi.getProductionData(projectFlockId) + ); + + if (isLoading) { + return ( +
    + +
    + ); + } + + if (!productionData || !isResponseSuccess(productionData)) { + return ( +
    + Gagal memuat data produksi. +
    + ); + } + + const { purchase, sales, performance } = productionData.data; + + // Helper for consistent row styling + const DataRow = ({ + label, + value, + unit = '', + valueClassName = 'font-bold text-gray-800', + unitClassName = 'text-gray-500 w-12 text-right', + }: { + label: string; + value: string | number; + unit?: string; + valueClassName?: string; + unitClassName?: string; + }) => ( +
    + {label} +
    + {value} + {unit && {unit}} +
    +
    + ); + + return ( +
    +

    Data Produksi

    + +
    + {/* Left Column */} +
    + {/* Purchase Section */} +
    +

    + Pembelian +

    +
    + + + + + + +
    +
    + + {/* Sales Section */} +
    +

    + Penjualan +

    +
    + {/* Chicken Sales */} +
    + + + + +
    + + {/* Egg Sales (if available) */} + {sales.egg && ( + <> +
    +
    + + + + +
    + + )} +
    +
    +
    + + {/* Divider Line (Absolute centered) */} +
    + + {/* Right Column */} +
    + {/* Performance Section */} +
    +

    + Performance +

    +
    + + + + + + + + + +
    +
    +
    +
    +
    + ); +}; + +export default ClosingProductionDataTabContent; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 91e78c8c..e6574d4f 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -15,6 +15,8 @@ import SelectInput, { import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; + import { cn, formatCurrency, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -43,17 +45,18 @@ const RowOptionsMenu = ({ }) => { return ( - {/* TODO: apply RBAC */}
    - + + +
    ); diff --git a/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx new file mode 100644 index 00000000..f683ec58 --- /dev/null +++ b/src/components/pages/closing/hpp-ekspedisi/HppExpeditionReportTable.tsx @@ -0,0 +1,110 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { ColumnDef } from '@tanstack/react-table'; +import Table from '@/components/Table'; +import Card from '@/components/Card'; +import { formatCurrency } from '@/lib/helper'; +import { BaseHppExpedition, BaseExpeditionCost } from '@/types/api/closing'; + +interface HppExpeditionReportTableProps { + type?: 'detail'; + initialValues?: BaseHppExpedition; +} + +const HppExpeditionReportTable = ({ + type = 'detail', + initialValues, +}: HppExpeditionReportTableProps) => { + const costOfRevenueExpeditionData: BaseExpeditionCost[] = useMemo(() => { + return initialValues?.expedition_costs || []; + }, [initialValues]); + + const totals = useMemo(() => { + const totalHpp = initialValues?.total_hpp_amount || 0; + + return { + totalHpp, + }; + }, [initialValues]); + + const costOfRevenueExpeditionColumns: ColumnDef[] = + useMemo( + () => [ + { + id: 'id', + accessorKey: 'id', + header: 'No', + cell: (props) => { + return
    {props.row.index + 1}
    ; + }, + footer: () => ( +
    + Total HPP Ekspedisi +
    + ), + }, + { + id: 'expedition_vendor_name', + accessorKey: 'expedition_vendor_name', + header: 'Nama Ekspedisi', + cell: (props) => props.getValue() || '-', + }, + { + id: 'hpp_amount', + accessorKey: 'hpp_amount', + header: 'HPP Ekspedisi', + cell: (props) => { + const value = props.getValue() as number; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(totals.totalHpp)} +
    + ), + }, + ], + [totals] + ); + + return ( + <> +
    +
    +

    HPP Ekspedisi

    + + 0} + className={{ + tableWrapperClassName: 'overflow-x-auto', + tableClassName: 'w-full table-auto text-sm', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', + 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 HppExpeditionReportTable; diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index 2b5b0a0a..c69f089f 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -4,6 +4,7 @@ import toast from 'react-hot-toast'; import Link from 'next/link'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; +import RequirePermission from '@/components/helper/RequirePermission'; import Card from '@/components/Card'; import DropFileInput from '@/components/input/DropFileInput'; @@ -62,16 +63,17 @@ const ExpenseRealizationContent = ({
    - {/* TODO: apply RBAC */} - + + +
    @@ -124,36 +126,38 @@ const ExpenseRealizationContent = ({ )}
    -
    - + +
    + - {formik.values.documents && - formik.values.documents.length > 0 && ( - - )} -
    + {formik.values.documents && + formik.values.documents.length > 0 && ( + + )} +
    + diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index 0d7d959d..b937c5bc 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -19,6 +19,7 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Expense } from '@/types/api/expense'; import { formatCurrency, formatDate } from '@/lib/helper'; @@ -255,100 +256,119 @@ const ExpenseRequestContent = ({
    {isCurrentApprovalOnManager && ( - + + + )} {isCurrentApprovalOnFinance && ( - + + + )} {isCurrentApprovalOnRealization && ( - + + + )} {showRejectButton && ( - + + )} {isExpenseCanBeRealized && ( - + + + )}
    {showEditButton && ( - + + + )} - + + +
    @@ -485,36 +505,42 @@ const ExpenseRequestContent = ({ )} -
    - + +
    + - {formik.values.documents && - formik.values.documents.length > 0 && ( - - )} -
    + {formik.values.documents && + formik.values.documents.length > 0 && ( + + )} +
    + diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index bbcb6c4e..9ae3ed34 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -28,6 +28,7 @@ import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge'; import CheckboxInput from '@/components/input/CheckboxInput'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; import DateInput from '@/components/input/DateInput'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; @@ -67,58 +68,70 @@ const RowOptionsMenu = ({ return (
    - - - {showEditButton && ( + + + + {showEditButton && ( + + + )} {showRealizationButton && ( - + + + )} - + + +
    ); @@ -559,57 +572,70 @@ const ExpensesTable = () => {
    - + + + {selectedRowIds.length > 0 && ( <> - + + + - + + + - + + )}
    diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx index a7ebdbca..d1c7c5f2 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx @@ -16,6 +16,7 @@ import DateInput from '@/components/input/DateInput'; import DropFileInput from '@/components/input/DropFileInput'; import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable'; import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense'; +import RequirePermission from '@/components/helper/RequirePermission'; import { CreateExpenseRealizationPayload, @@ -290,21 +291,23 @@ const ExpenseRealizationForm = ({ className={{ wrapper: 'col-span-12' }} /> - + + + {formik.values.existing_documents && formik.values.existing_documents.length > 0 && ( @@ -357,20 +360,22 @@ const ExpenseRealizationForm = ({ {type !== 'add' && (
    {type !== 'edit' && ( - + + + )}
    )} diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index d52bde0d..71160785 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -18,6 +18,7 @@ import DateInput from '@/components/input/DateInput'; import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable'; import DropFileInput from '@/components/input/DropFileInput'; import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense'; +import RequirePermission from '@/components/helper/RequirePermission'; import { ExpenseRequestFormSchema, @@ -385,21 +386,23 @@ const ExpenseRequestForm = ({ className={{ wrapper: 'col-span-12' }} /> - + + + {formik.values.existing_documents && formik.values.existing_documents.length > 0 && ( @@ -461,36 +464,40 @@ const ExpenseRequestForm = ({
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index a3de8a34..7612a081 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -4,6 +4,7 @@ import Badge from '@/components/Badge'; import Button from '@/components/Button'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import Table from '@/components/Table'; +import RequirePermission from '@/components/helper/RequirePermission'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; @@ -175,15 +176,17 @@ const InventoryAdjustmentTable = () => {
    - + + + {/* ; }) => ( - + + + ); @@ -145,15 +148,17 @@ const MovementTable = () => {
    - + + +
    ; }) => ( - + + + ); diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index 49927e05..f9cbdbfe 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -26,6 +26,8 @@ import { useRouter } from 'next/navigation'; import { useCallback, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; +import RequirePermission from '@/components/helper/RequirePermission'; +import { useAuth } from '@/services/hooks/useAuth'; const RowsOptionsMenu = ({ type = 'dropdown', @@ -50,57 +52,71 @@ const RowsOptionsMenu = ({ )} >
    - - {props.row.original.latest_approval.step_number != 1 && ( + + + {props.row.original.latest_approval.step_number != 1 && ( + + + )} {props.row.original.latest_approval.step_number != 3 && ( - + + + )} - + + +
    ); @@ -116,6 +132,7 @@ const MarketingTable = () => { ); const [selectedItem, setSelectedItem] = useState(null); const [rowSelection, setRowSelection] = useState>({}); + const { permissionCheck } = useAuth(); const router = useRouter(); @@ -270,10 +287,14 @@ const MarketingTable = () => {
    { }} />
    - + + + - + + +
    {initialValues?.latest_approval?.step_number == 1 && ( <> - - + + + + + + + )} {initialValues?.latest_approval?.step_number != 1 && ( - + + )}
    @@ -413,19 +427,23 @@ const MarketingDetail = ({ )}
    {initialValues?.latest_approval?.step_number != 3 && ( - + + + )} - + + +
    - + + +
    )} diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index 207fb8a6..45c4fdff 100644 --- a/src/components/pages/master-data/area/AreasTable.tsx +++ b/src/components/pages/master-data/area/AreasTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Area } from '@/types/api/master-data/area'; import { AreaApi } from '@/services/api/master-data'; @@ -34,40 +35,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -192,15 +199,19 @@ const AreasTable = () => {
    - +
    + + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx index 58b09ef8..f28f4bd0 100644 --- a/src/components/pages/master-data/bank/BanksTable.tsx +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Bank } from '@/types/api/master-data/bank'; import { BankApi } from '@/services/api/master-data'; @@ -34,40 +35,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -205,15 +212,17 @@ const BanksTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/customer/CustomersTable.tsx b/src/components/pages/master-data/customer/CustomersTable.tsx index 89401638..3e442620 100644 --- a/src/components/pages/master-data/customer/CustomersTable.tsx +++ b/src/components/pages/master-data/customer/CustomersTable.tsx @@ -9,6 +9,7 @@ import Table from '@/components/Table'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; @@ -32,38 +33,44 @@ const RowOptionsMenu = ({ }) => { return ( - - - + > + + Detail + + + + + + + + ); }; @@ -200,15 +207,17 @@ const CustomersTable = () => {
    - + + +
    {formType !== 'add' && (
    - - - {formType !== 'edit' && ( + + + + {formType !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/fcr/FcrsTable.tsx b/src/components/pages/master-data/fcr/FcrsTable.tsx index b582222e..2d65a406 100644 --- a/src/components/pages/master-data/fcr/FcrsTable.tsx +++ b/src/components/pages/master-data/fcr/FcrsTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Fcr } from '@/types/api/master-data/fcr'; import { FcrApi } from '@/services/api/master-data'; @@ -34,40 +35,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -192,15 +199,17 @@ const FcrsTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx index 5350c518..ce8f701a 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -13,6 +13,7 @@ import { useModal } from '@/components/Modal'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import toast from 'react-hot-toast'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; @@ -32,48 +33,54 @@ const RowsOptions = ({ }) => { return ( - - + + + - + > + + Detail + + + + + ); }; @@ -196,15 +203,17 @@ const FlockTable = () => {
    - + + +
    {
    {formType !== 'add' && (
    - - {formType !== 'edit' && ( + + + {formType !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx index eebc490a..1bd7badb 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Kandang } from '@/types/api/master-data/kandang'; import { KandangApi } from '@/services/api/master-data'; @@ -39,40 +40,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -243,15 +250,19 @@ const KandangsTable = () => {
    - +
    + + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx index 19f11298..10fe46c9 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Location } from '@/types/api/master-data/location'; import { LocationApi } from '@/services/api/master-data'; @@ -39,40 +40,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -230,15 +237,19 @@ const LocationsTable = () => {
    - +
    + + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index ae38c573..7066c19a 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { NonstockApi } from '@/services/api/master-data'; @@ -39,40 +40,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -242,15 +249,17 @@ const NonstocksTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index 1a6e641c..a9b98bcb 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; @@ -34,38 +35,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - + > + + Detail + + + + + + + + + + ); }; @@ -193,15 +202,17 @@ const ProductCategoryTable = () => {
    - + + +
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index 2a94656c..957d0551 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Product } from '@/types/api/master-data/product'; import { ProductApi } from '@/services/api/master-data'; @@ -38,38 +39,44 @@ const RowOptionsMenu = ({ deleteClickHandler: () => void; }) => ( - - - + > + + Detail + + + + + + + + ); @@ -273,15 +280,17 @@ const ProductsTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - {type !== 'edit' && ( + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index da84afc0..3e10c9c8 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -9,6 +9,7 @@ import Table from '@/components/Table'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { ROWS_OPTIONS } from '@/config/constant'; import { isResponseSuccess } from '@/lib/api-helper'; import { cn } from '@/lib/helper'; @@ -32,48 +33,54 @@ const RowOptions = ({ }) => { return ( - - + + + - + > + + Edit + + + + + ); }; @@ -219,15 +226,17 @@ const SuppliersTable = () => {
    - + + +
    {formType !== 'add' && (
    - - - {formType !== 'edit' && ( + + + + {formType !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/uom/UomsTable.tsx b/src/components/pages/master-data/uom/UomsTable.tsx index edf67f34..851647b9 100644 --- a/src/components/pages/master-data/uom/UomsTable.tsx +++ b/src/components/pages/master-data/uom/UomsTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Uom } from '@/types/api/master-data/uom'; import { UomApi } from '@/services/api/master-data'; @@ -34,40 +35,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -192,15 +199,17 @@ const UomsTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/master-data/warehouse/WarehousesTable.tsx b/src/components/pages/master-data/warehouse/WarehousesTable.tsx index a61f6f5b..fe694322 100644 --- a/src/components/pages/master-data/warehouse/WarehousesTable.tsx +++ b/src/components/pages/master-data/warehouse/WarehousesTable.tsx @@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { WarehouseApi } from '@/services/api/master-data'; @@ -39,40 +40,46 @@ const RowOptionsMenu = ({ }) => { return ( - - - - - + > + + Detail + + + + + + + + + + ); }; @@ -270,15 +277,17 @@ const WarehousesTable = () => {
    - + + +
    {
    {type !== 'add' && (
    - - - {type !== 'edit' && ( + + + + {type !== 'edit' && ( + + + )}
    )} diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 63b94cc8..b3aa69a0 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -25,6 +25,8 @@ import { ChangeEventHandler, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import useSWR from 'swr'; +import RequirePermission from '@/components/helper/RequirePermission'; + const RowOptionsMenu = ({ type = 'dropdown', props, @@ -46,50 +48,58 @@ const RowOptionsMenu = ({ )} >
    - - {props.row.original.approval.step_name === 'Aktif' && ( + + + {props.row.original.approval.step_name === 'Aktif' && ( + + + )} {props.row.original.approval.step_name === 'Pengajuan' && ( - + + + )} - + + +
    ); @@ -287,14 +297,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
    - + + + {/*
    - + + +
    ))}
    diff --git a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx index 41b511c9..0ee3ae32 100644 --- a/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx +++ b/src/components/pages/production/project-flock/detail/ProjectFlockDetail.tsx @@ -29,6 +29,7 @@ import { } from '@/config/approval-line'; import useSWR from 'swr'; import { ProjectFlockKandangApi } from '@/services/api/production'; +import RequirePermission from '@/components/helper/RequirePermission'; const ProjectFlockDetail = ({ projectFlock, @@ -110,27 +111,31 @@ const ProjectFlockDetail = ({ leftIconHref='/production/project-flock' subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`} > - - - - - - + + + + + + + + + + {/* Informasi Umum */} @@ -418,38 +423,42 @@ const ProjectFlockDetail = ({
    - - - - - + + + + - Close - - + + +
    diff --git a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx index 5ce62733..46830879 100644 --- a/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx +++ b/src/components/pages/production/project-flock/form/ProjectFlockForm.tsx @@ -47,6 +47,7 @@ import Card from '@/components/Card'; import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { useUiStore } from '@/stores/ui/ui.store'; +import RequirePermission from '@/components/helper/RequirePermission'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; interface ProjectFlockFormProps { @@ -734,36 +735,40 @@ const ProjectFlockForm = ({ )} {formType == 'detail' && (
    - - + + + + + +
    )}
    */} {formType !== 'detail' && ( - + + )}
    diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 65cead2a..e5bd30cb 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -6,6 +6,7 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { SortingState, CellContext } from '@tanstack/react-table'; import { cn, formatDate } from '@/lib/helper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { useModal } from '@/components/Modal'; import Modal from '@/components/Modal'; import Button from '@/components/Button'; @@ -59,60 +60,70 @@ const RowOptionsMenu = ({ return ( - - - {!isApproved && !isRejected && ( + + + + + + {!isApproved && !isRejected && ( + + + )} {!isApproved && !isRejected && ( + + + + )} + - )} - + ); }; @@ -514,49 +525,63 @@ const RecordingTable = () => {
    - + + + {selectedRowIds.length > 0 && ( <> - + + + - + + + )}
    diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 4dca38dc..c8fa3ca0 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -8,6 +8,7 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; +import RequirePermission from '@/components/helper/RequirePermission'; import Card from '@/components/Card'; import Badge from '@/components/Badge'; import NumberInput from '@/components/input/NumberInput'; @@ -1492,41 +1493,45 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { !isRecordingApproved(initialValues) && !isRecordingRejected(initialValues) && (
    - + + + - + + +
    )}
    @@ -2696,36 +2701,40 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { {/* Left side - Detail & Edit actions */}
    {type === 'detail' && deleteRecordingClickHandler && ( - + + + )} {type === 'detail' && initialValues && ( - + + + )}
    {/* Right side actions */} diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 424cc3c2..18ce404d 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -26,6 +26,7 @@ import TextInput from '@/components/input/TextInput'; import CheckboxInput from '@/components/input/CheckboxInput'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes'; +import RequirePermission from '@/components/helper/RequirePermission'; import { TransferToLaying } from '@/types/api/production/transfer-to-laying'; import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying'; @@ -56,72 +57,81 @@ const RowOptionsMenu = ({ const showDeleteButton = showEditButton; - // TODO: apply RBAC const showApproveButton = showEditButton; const showRejectButton = showEditButton; return ( - - - {showEditButton && ( + + + + {showEditButton && ( + + + )} {/* TODO: apply RBAC */} {showApproveButton && ( - + + + )} {showRejectButton && ( - + + + )} {showDeleteButton && ( - + + + )} ); @@ -502,47 +512,53 @@ const TransferToLayingsTable = () => {
    - + + + {selectedRowIds.length > 0 && ( <> - + + + - + + + )}
    diff --git a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx index 16885062..4d60f69a 100644 --- a/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx +++ b/src/components/pages/production/transfer-to-laying/form/TransferToLayingForm.tsx @@ -8,6 +8,7 @@ import useSWR from 'swr'; import { Icon } from '@iconify/react'; import Button from '@/components/Button'; +import RequirePermission from '@/components/helper/RequirePermission'; import SelectInput, { OptionType, useSelect, @@ -500,34 +501,37 @@ const TransferToLayingForm = ({ <> {isShowApproveRejectButton && (
    - {/* TODO: apply RBAC */} - + + + - + + +
    )} @@ -788,37 +792,41 @@ const TransferToLayingForm = ({ {type !== 'add' && (
    {isShowDeleteButton && ( - + + + )} {type !== 'edit' && isShowEditButton && ( - + + + )}
    )} @@ -833,15 +841,23 @@ const TransferToLayingForm = ({ Reset - + +
    )}
    diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index a77e1158..81f45cc9 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RowDropdownOptions from '@/components/table/RowDropdownOptions'; import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import RequirePermission from '@/components/helper/RequirePermission'; import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -38,15 +39,17 @@ const RowOptionsMenu = ({ }: RowOptionsMenuProps) => { return ( - + + + {/**/} - + + + ); }; @@ -227,15 +232,17 @@ const PurchaseTable = () => {
    - + + +
    {canUpdatePurchaseItems && canShowDeleteAddButtons && ( - + + + )}
    diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index 35d9e104..de07ee52 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -39,19 +39,22 @@ import { toast } from 'react-hot-toast'; import { useSearchParams } from 'next/navigation'; import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line'; +import RequirePermission from '@/components/helper/RequirePermission'; const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => { return ( - + + + ); }; @@ -59,15 +62,17 @@ const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => { const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => { return ( - + + + ); }; @@ -496,14 +501,16 @@ const PurchaseOrderDetail = ({ }; return ( - + + + ); }, }, @@ -632,25 +639,45 @@ const PurchaseOrderDetail = ({ {showApprovalButton && (
    - + + - + +
    )}
    diff --git a/src/components/pages/report/DailyMarketingReportContent.tsx b/src/components/pages/report/DailyMarketingReportContent.tsx new file mode 100644 index 00000000..1eba4ea3 --- /dev/null +++ b/src/components/pages/report/DailyMarketingReportContent.tsx @@ -0,0 +1,413 @@ +'use client'; + +import { ChangeEventHandler, useState } from 'react'; +import { pdf } from '@react-pdf/renderer'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Dropdown from '@/components/dropdown/Dropdown'; +import DateInput from '@/components/input/DateInput'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import Menu from '@/components/menu/Menu'; +import MenuItem from '@/components/menu/MenuItem'; +import DailyMarketingsTable from '@/components/pages/report/DailyMarketingsTable'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import DailyMarketingReportPDF from '@/components/pages/report/DailyMarketingReportPDF'; + +import { Area } from '@/types/api/master-data/area'; +import { + AreaApi, + CustomerApi, + LocationApi, + WarehouseApi, +} from '@/services/api/master-data'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { Customer } from '@/types/api/master-data/customer'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; +import { MARKETING_TYPE_OPTIONS } from '@/config/constant'; +import { httpClient } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { isResponseError } from '@/lib/api-helper'; + +const DailyMarketingReportContent = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + reset: resetFilter, + } = useTableFilter({ + initial: { + search: '', + area_id: '', + location_id: '', + warehouse_id: '', + customer_id: '', + start_date: '', + end_date: '', + marketing_type: '', + filter_by: '', + sort_by: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + area_id: 'area_id', + location_id: 'location_id', + warehouse_id: 'warehouse_id', + customer_id: 'customer_id', + start_date: 'start_date', + end_date: 'end_date', + marketing_type: 'marketing_type', + filter_by: 'filter_by', + sort_by: 'sort_by', + }, + }); + + const dailyMarketingsReportUrl = `${MarketingReportApi.basePath}${getTableFilterQueryString()}`; + + const [isLoadingExportingToExcel, setIsLoadingExportingToExcel] = + useState(false); + const [isLoadingExportingToPdf, setIsLoadingExportingToPdf] = useState(false); + + const [selectedArea, setSelectedArea] = useState(null); + const { + setInputValue: setAreaInputValue, + options: areaOptions, + isLoadingOptions: isLoadingAreaOptions, + } = useSelect(AreaApi.basePath, 'id', 'name'); + + const areaChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedArea(val as OptionType); + updateFilter('area_id', val ? ((val as OptionType).value as string) : ''); + }; + + const [selectedLocation, setSelectedLocation] = useState( + null + ); + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + updateFilter( + 'location_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedWarehouse, setSelectedWarehouse] = useState( + null + ); + const { + setInputValue: setWarehouseInputValue, + options: warehouseOptions, + isLoadingOptions: isLoadingWarehouseOptions, + } = useSelect(WarehouseApi.basePath, 'id', 'name'); + + const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedWarehouse(val as OptionType); + updateFilter( + 'warehouse_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedCustomer, setSelectedCustomer] = useState( + null + ); + const { + setInputValue: setCustomerInputValue, + options: customerOptions, + isLoadingOptions: isLoadingCustomerOptions, + } = useSelect(CustomerApi.basePath, 'id', 'name'); + + const customerChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedCustomer(val as OptionType); + updateFilter( + 'customer_id', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const startDateChangeHandler = (e: React.ChangeEvent) => { + updateFilter('start_date', e.target.value ? e.target.value : ''); + }; + + const endDateChangeHandler = (e: React.ChangeEvent) => { + updateFilter('end_date', e.target.value ? e.target.value : ''); + }; + + const [selectedMarketingType, setSelectedMarketingType] = + useState(null); + const marketingTypeChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedMarketingType(val as OptionType); + updateFilter( + 'marketing_type', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const filterByChangeHandler = (filterBy: string) => { + updateFilter('filter_by', filterBy); + }; + + const sortByChangeHandler = (sort: 'asc' | 'desc' | '') => { + updateFilter('sort_by', sort); + }; + + const exportToExcelHandler = async () => { + setIsLoadingExportingToExcel(true); + + await MarketingReportApi.exportDailyMarketingToExcel( + getTableFilterQueryString() + ); + + setIsLoadingExportingToExcel(false); + }; + + const exportToPdfHandler = async () => { + setIsLoadingExportingToPdf(true); + + const params = new URLSearchParams(getTableFilterQueryString()); + + params.set('limit', '9999999'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClient< + BaseApiResponse + >(`${MarketingReportApi.basePath}${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + return; + } + + const openPdf = async () => { + const dailyMarketingReportPdfBlob = await pdf( + + ).toBlob(); + + const dailyMarketingReportPdfUrl = URL.createObjectURL( + dailyMarketingReportPdfBlob + ); + window.open(dailyMarketingReportPdfUrl, '_blank'); + }; + + const downloadPdf = async () => { + const blob = await pdf( + + ).toBlob(); + const url = URL.createObjectURL(blob); + + const link = document.createElement('a'); + link.href = url; + link.download = 'laporan-penjualan-harian.pdf'; + link.click(); + + URL.revokeObjectURL(url); + }; + + await openPdf(); + } catch (error) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + } + + setIsLoadingExportingToPdf(false); + }; + + const handleReset = () => { + setSelectedArea(null); + setSelectedLocation(null); + setSelectedWarehouse(null); + setSelectedCustomer(null); + setSelectedMarketingType(null); + resetFilter(); + }; + + return ( +
    +
    +

    Penjualan Harian

    +
    + + {/* Filters */} +
    +
    + + + + + + + + + + + +
    + +
    + + +
    + + + + + + Export{' '} + + + } + > + + + + + +
    +
    +
    + + +
    + ); +}; + +export default DailyMarketingReportContent; diff --git a/src/components/pages/report/DailyMarketingReportPDF.tsx b/src/components/pages/report/DailyMarketingReportPDF.tsx new file mode 100644 index 00000000..337892b3 --- /dev/null +++ b/src/components/pages/report/DailyMarketingReportPDF.tsx @@ -0,0 +1,550 @@ +'use client'; + +import { + Document, + Image, + Page, + StyleSheet, + Text, + View, +} from '@react-pdf/renderer'; + +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; + +interface DailyMarketingReportPDFProps { + data?: DailyMarketingReport; +} + +const DailyMarketingReportPDFStyle = StyleSheet.create({ + page: { + paddingTop: 24, + paddingBottom: 64, + paddingHorizontal: 16, // Reduce padding to fit more columns + orientation: 'landscape', + }, + + companyInfoHeader: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: 8, + }, + companyLogo: { + width: 64, + height: 'auto', + }, + companyInfoHeaderDate: { + paddingTop: 8, + fontSize: 10, + }, + companyName: { + fontSize: 12, + fontWeight: 'bold', + marginBottom: 4, + }, + companyAddress: { + fontSize: 8, + maxWidth: 400, + marginBottom: 10, + }, + + title: { + marginTop: 16, + fontSize: 14, + lineHeight: '150%', + textAlign: 'center', + fontFamily: 'Times-Roman', + fontWeight: 'bold', + }, + + footer: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + + position: 'absolute', + fontSize: 8, + bottom: 30, + left: 0, + right: 0, + textAlign: 'center', + color: 'grey', + }, + + // Table Styles + table: { + width: '100%', + marginTop: 16, + borderWidth: 1, + borderColor: '#000000', + borderBottomWidth: 0, + fontSize: 7, // Smaller font for report + }, + tableRow: { + flexDirection: 'row', + borderBottomWidth: 1, + borderBottomColor: '#000000', + alignItems: 'center', + minHeight: 20, + }, + tableHeader: { + backgroundColor: '#f0f0f0', + fontWeight: 'bold', + }, + + // Columns definition (Total 100%) + colNo: { + width: '3%', + padding: 2, + textAlign: 'center', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSoDate: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colDoDate: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colAging: { + width: '3%', + padding: 2, + textAlign: 'center', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colWarehouse: { + width: '7%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colCustomer: { + width: '9%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, // Reduced slightly + colSales: { + width: '6%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colProduct: { + width: '8%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, // Reduced slightly + colDoNumber: { + width: '7%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colVehicle: { + width: '5%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colMarketingType: { + width: '5%', + padding: 2, + textAlign: 'left', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colQty: { + width: '4%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colAvgWeight: { + width: '4%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colTotalWeight: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSalesPrice: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colHppPrice: { + width: '5%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colSalesAmount: { + width: '6%', + padding: 2, + textAlign: 'right', + borderRightWidth: 1, + borderRightColor: '#000000', + }, + colHppAmount: { width: '6%', padding: 2, textAlign: 'right' }, // Last column + + // Text inside columns + cellText: { + fontSize: 6, + }, + headerText: { + fontSize: 7, + fontWeight: 'bold', + textAlign: 'center', + }, + + // Utils + doubleDivider: { + width: '100%', + height: 6, + borderTop: '2px solid black', + borderBottom: '2px solid black', + }, + + // Summary + summaryContainer: { + marginTop: 12, + flexDirection: 'row', + justifyContent: 'flex-end', + width: '100%', + }, + summaryTable: { + width: '30%', + borderWidth: 1, + borderColor: '#000000', + fontSize: 8, + }, + summaryRow: { + flexDirection: 'row', + padding: 2, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + summaryLabel: { + width: '50%', + fontWeight: 'bold', + }, + summaryValue: { + width: '50%', + textAlign: 'right', + }, +}); + +const DailyMarketingReportPDF = ({ data }: DailyMarketingReportPDFProps) => { + const rows = data?.rows || []; + const summary = data?.summary; + + return ( + + + + + + + + {formatDate(Date.now(), 'DD MMMM YYYY')} + + + + + + PT LUMBUNG TELUR INDONESIA + + + SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel. + Cipedes, Kec. Sukajadi, Kota Bandung 40162 + + + + + + + + Laporan Penjualan Harian + + + {/* Data Table */} + + {/* Header */} + + + No + + + + Tgl SO + + + + + Tgl DO + + + + Aging + + + + Gudang + + + + + Pelanggan + + + + Sales + + + + Produk + + + + No DO + + + + Plat No + + + + Tipe + + + Qty + + + + Rerata + + + + Berat + + + + Hrg Jual + + + + + HPP/kg + + + + + Total Jual + + + + + Total HPP + + + + + {/* Rows */} + {rows.map((row, index) => ( + + + + {index + 1} + + + + + {formatDate(row.so_date, 'DD/MM/YYYY')} + + + + + {formatDate(row.do_date, 'DD/MM/YYYY')} + + + + + {row.aging_days} + + + + + {row.warehouse?.name} + + + + + {row.customer?.name} + + + + + {row.sales} + + + + + {row.product?.name} + + + + + {row.do_number} + + + + + {row.vehicle_number} + + + + + {row.marketing_type} + + + + + {formatNumber(row.qty)} + + + + + {formatNumber(row.average_weight_kg)} + + + + + {formatNumber(row.total_weight_kg)} + + + + + {formatCurrency(row.sales_price_per_kg)} + + + + + {formatCurrency(row.hpp_price_per_kg)} + + + + + {formatCurrency(row.sales_amount)} + + + + + {formatCurrency(row.hpp_amount)} + + + + ))} + + + {/* Summary */} + + + + + Total Qty: + + + {formatNumber(summary?.total_qty ?? 0)} + + + + + Total Berat (kg): + + + {formatNumber(summary?.total_weight_kg ?? 0)} + + + + + Total Penjualan: + + + {formatCurrency(summary?.total_sales_amount ?? 0)} + + + + + Total HPP: + + + {formatCurrency(summary?.total_hpp_amount ?? 0)} + + + + + + + + `${pageNumber} / ${totalPages}` + } + fixed + /> + + + + ); +}; + +export default DailyMarketingReportPDF; diff --git a/src/components/pages/report/DailyMarketingsTable.tsx b/src/components/pages/report/DailyMarketingsTable.tsx new file mode 100644 index 00000000..d6914cf1 --- /dev/null +++ b/src/components/pages/report/DailyMarketingsTable.tsx @@ -0,0 +1,255 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { DailyMarketingRow } from '@/types/api/report/marketing'; +import { MarketingReportApi } from '@/services/api/report/marketing-report'; + +interface DailyMarketingsTableProps { + dailyMarketingsReportUrl: string; + onSetPage: (page: number) => void; + pageSize: number; + onSetPageSize: (pageSize: number) => void; + searchValue: string; + onSearchChange: ChangeEventHandler; + onFilterByChange: (filterBy: string) => void; + onSortByChange: (sort: 'asc' | 'desc' | '') => void; +} + +const DailyMarketingsTable = ({ + dailyMarketingsReportUrl, + onSetPage, + pageSize, + onSetPageSize, + searchValue, + onSearchChange, + onFilterByChange, + onSortByChange, +}: DailyMarketingsTableProps) => { + const { data: dailyMarketings, isLoading: isLoadingDailyMarketings } = useSWR( + dailyMarketingsReportUrl, + MarketingReportApi.getAllDailyMarketingFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + + const dailyMarketingColumns: ColumnDef[] = [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'so_date', + header: 'Tanggal Jual', + cell: (props) => formatDate(props.row.original.so_date, 'DD-MMM-YYYY'), + footer: 'Total', + }, + { + accessorKey: 'do_date', + header: 'Tanggal DO', + cell: (props) => formatDate(props.row.original.do_date, 'DD-MMM-YYYY'), + }, + { + accessorKey: 'aging_days', + header: 'Aging', + cell: (props) => `${props.row.original.aging_days} hari`, + }, + { + accessorKey: 'warehouse.name', + header: 'Gudang', + }, + { + accessorKey: 'customer.name', + header: 'Pelanggan', + }, + { + accessorKey: 'do_number', + header: 'No. DO', + }, + { + accessorKey: 'sales', + header: 'Sales/Marketing', + }, + { + accessorKey: 'vehicle_number', + header: 'No. Polisi', + cell: (props) => ( + {props.row.original.vehicle_number} + ), + }, + { + accessorKey: 'marketing_type', + header: 'Marketing Type', + }, + { + accessorKey: 'product.name', + header: 'Produk', + }, + { + accessorKey: 'qty', + header: 'Kuantitas', + cell: (props) => formatNumber(props.row.original.qty), + footer: () => { + const totalQty = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_qty + : 0; + + return formatNumber(totalQty); + }, + }, + { + accessorKey: 'average_weight_kg', + header: 'Bobot Rata-Rata (Kg)', + cell: (props) => formatNumber(props.row.original.average_weight_kg), + }, + { + accessorKey: 'total_weight_kg', + header: 'Bobot Total (Kg)', + cell: (props) => formatNumber(props.row.original.total_weight_kg), + footer: () => { + const totalWeightKg = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_weight_kg + : 0; + + return formatNumber(totalWeightKg); + }, + }, + { + accessorKey: 'sales_price_per_kg', + header: 'Harga Jual (Rp)', + cell: (props) => formatCurrency(props.row.original.sales_price_per_kg), + }, + { + accessorKey: 'hpp_price_per_kg', + header: 'HPP (Rp)', + cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg), + }, + { + accessorKey: 'sales_amount', + header: 'Total (Rp)', + cell: (props) => formatCurrency(props.row.original.sales_amount), + footer: () => { + const totalSalesAmount = isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.summary.total_sales_amount + : 0; + + return formatCurrency(totalSalesAmount); + }, + }, + ]; + + useEffect(() => { + if (sorting.length === 1) { + onFilterByChange(sorting[0].id); + onSortByChange(sorting[0].desc ? 'desc' : 'asc'); + } else { + onFilterByChange(''); + onSortByChange(''); + } + }, [sorting]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(dailyMarketings) + ? dailyMarketings.data.rows.length > 0 + : false + ); + } + }, [dailyMarketings, isResponseSuccess]); + + return ( + + +
    Penjualan Harian
    + + +
    + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.data.rows + : [] + } + columns={dailyMarketingColumns} + pageSize={pageSize} + onPageSizeChange={onSetPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(dailyMarketings) + ? dailyMarketings?.meta?.total_results + : 0 + } + onPageChange={onSetPage} + isLoading={isLoadingDailyMarketings} + sorting={sorting} + setSorting={setSorting} + renderFooter={true} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(dailyMarketings) && + dailyMarketings?.data?.rows.length === 0, + }), + }} + /> +
    + + + ); +}; + +export default DailyMarketingsTable; diff --git a/src/components/pages/report/MarketingReportContent.tsx b/src/components/pages/report/MarketingReportContent.tsx new file mode 100644 index 00000000..d54c935a --- /dev/null +++ b/src/components/pages/report/MarketingReportContent.tsx @@ -0,0 +1,50 @@ +'use client'; + +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' + | 'transaction' + | 'hpp-comparison' + | 'daily-hpp'; + +const marketingReportTabs: { + id: MarketingReportTabType; + label: string; + content: JSX.Element; +}[] = [ + { + id: 'daily', + label: 'Penjualan Harian', + content: , + }, + { + id: 'daily-hpp', + label: 'HPP Harian Kandang', + content: , + }, +]; + +const MarketingReportContent = () => { + const [activeTab, setActiveTab] = useState('daily'); + + return ( +
    + +
    + ); +}; + +export default MarketingReportContent; diff --git a/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx new file mode 100644 index 00000000..1e2d2824 --- /dev/null +++ b/src/components/pages/report/logistic-stock/LogisticStockTabs.tsx @@ -0,0 +1,32 @@ +'use client'; + +import Tabs from '@/components/Tabs'; +import PurchasesPerSupplierTab from '@/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab'; + +const LogisticStockTabs = () => { + const tabs = [ + { + id: '1', + label: 'Rekapitulasi Pembelian Per Supplier', + content: , + }, + // { + // id: '2', + // label: 'Rekapitulasi Pemakaian Barang', + // content: 'Rekapitulasi Pemakaian Barang Tab', + // }, + // { + // id: '3', + // label: 'Rekapitulasi Stock Persediaan Barang', + // content: 'Rekapitulasi Stock Persediaan Barang Tab', + // }, + ]; + + return ( +
    + +
    + ); +}; + +export default LogisticStockTabs; diff --git a/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx new file mode 100644 index 00000000..a7967159 --- /dev/null +++ b/src/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport.tsx @@ -0,0 +1,404 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; +import { formatCurrency, 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', + }, + tableCellNo: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableCellLast: { + flex: 1, + padding: 4, + fontSize: 8, + }, + 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, + }, + tableCellHeaderLast: { + flex: 1, + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + 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', + }, + tableCellCenterLast: { + flex: 1, + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + supplierSection: { + marginBottom: 10, + }, + supplierSectionBreak: { + marginBottom: 15, + }, + badge: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + padding: 2, + borderRadius: 2, + fontSize: 7, + fontWeight: 'bold', + alignSelf: 'center', + marginRight: 4, + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, +}); + +interface PurchasesPerSupplierExportParams { + data: LogisticPurchasePerSupplierReport[]; + params: { + area_name?: string; + supplier_name?: string; + product_name?: string; + product_category_name?: string; + received_date?: string; + po_date?: string; + start_date?: string; + end_date?: string; + sort_by?: string; + filter_by?: string; + }; +} + +const getParameterText = ( + params: PurchasesPerSupplierExportParams['params'] +) => { + const paramsText = []; + + if (params.supplier_name) { + paramsText.push(`Supplier: ${params.supplier_name}`); + } else { + paramsText.push('Semua Supplier'); + } + + if (params.start_date && params.end_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + const endDate = formatDate(params.end_date, 'DD MMM YYYY'); + paramsText.push(`Periode: ${startDate} - ${endDate}`); + } else if (params.start_date) { + const startDate = formatDate(params.start_date, 'DD MMM YYYY'); + paramsText.push(`Tanggal: ${startDate}`); + } + + const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm'); + paramsText.push(`Dicetak: ${currentDate}`); + + return paramsText; +}; + +const createPDFDocument = ( + supplierReports: LogisticPurchasePerSupplierReport[], + params: PurchasesPerSupplierExportParams['params'] +) => ( + + + {/* Title and Parameters */} + + + Laporan > Rekapitulasi Pembelian Per Supplier + + + + + Jenis Tanggal:{' '} + {params.filter_by === 'received_date' + ? 'Tanggal Terima' + : 'Tanggal PO'} + + + {getParameterText(params).map((param, index) => ( + + {param} + + ))} + + + + {/* Supplier Sections */} + {supplierReports.map( + ( + supplierReport: LogisticPurchasePerSupplierReport, + supplierIndex: number + ) => { + return ( + + + {supplierReport.supplier.name} + + + + {/* Table Header */} + + + No + + + Tanggal Terima + + + Tanggal PO + + + Referensi + + + Produk + + + Tujuan + + + Qty + + + Harga Beli + + + Nilai Pembelian + + + Biaya Transport + + + Total + + + Armada + + + Surat Jalan + + + + {/* Table Body */} + {supplierReport.rows.map( + ( + item: LogisticPurchasePerSupplierReport['rows'][number], + index: number + ) => ( + + + {index + 1} + + + + {formatDate(item.receive_date, 'DD-MMM-YYYY')} + + + + {formatDate(item.po_date, 'DD-MMM-YYYY')} + + + {item.po_number || '-'} + + + {item.product?.name || '-'} + + + {item.warehouse?.name || '-'} + + + {formatNumber(item.qty || 0)} + + + {formatCurrency(item.unit_price || 0)} + + + {formatCurrency(item.purchase_value || 0)} + + + + {formatCurrency(item.transport_unit_price || 0)} + + + + {formatCurrency(item.total_amount || 0)} + + + + {item.expedition || '-'} + + + + {item.delivery_number || '-'} + + + ) + )} + + + ); + } + )} + + +); + +export const generatePurchasesPerSupplierPDF = async ( + data: LogisticPurchasePerSupplierReport[], + params: PurchasesPerSupplierExportParams['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-pembelian-per-supplier-dicetak-pada-${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/logistic-stock/tab/PurchasesPerSupplierTab.tsx b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx new file mode 100644 index 00000000..c01eeb61 --- /dev/null +++ b/src/components/pages/report/logistic-stock/tab/PurchasesPerSupplierTab.tsx @@ -0,0 +1,934 @@ +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 { AreaApi } from '@/services/api/master-data'; +import { SupplierApi } from '@/services/api/master-data'; +import { ProductApi } from '@/services/api/master-data'; +import { ProductCategoryApi } from '@/services/api/master-data'; +import { LogisticApi } from '@/services/api/report/logistic-stock'; +import Table from '@/components/Table'; +import { ColumnDef } from '@tanstack/react-table'; +import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; +import { + LogisticPurchasePerSupplierReport, + LogisticPurchasePerSupplierSummary, +} from '@/types/api/report/logistic-stock'; +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'; +import MenuItem from '@/components/menu/MenuItem'; +import Menu from '@/components/menu/Menu'; +import { generatePurchasesPerSupplierPDF } from '@/components/pages/report/logistic-stock/export/PurchasesPerSupplierExport'; +import toast from 'react-hot-toast'; +import * as XLSX from 'xlsx'; + +const PurchasesPerSupplierTab = () => { + // ===== STATE MANAGEMENT ===== + const [isPdfExportLoading, setIsPdfExportLoading] = useState(false); + const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); + const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; + + // ===== 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: [] as string[], + supplier_id: [] as string[], + product_id: [] as string[], + product_category_id: [] as string[], + received_date: '', + po_date: '', + start_date: '', + end_date: '', + sort_by: '', + filter_by: 'received_date', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, + }); + + const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( + AreaApi.basePath, + 'id', + 'name', + 'search' + ); + + const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = + useSelect(SupplierApi.basePath, 'id', 'name', 'search', { + category: 'SAPRONAK', + }); + + const { options: productOptions, isLoadingOptions: isLoadingProducts } = + useSelect(ProductApi.basePath, 'id', 'name', 'search'); + + const { + options: productCategoryOptions, + isLoadingOptions: isLoadingProductCategories, + } = useSelect(ProductCategoryApi.basePath, 'id', 'name', 'search'); + + const dataTypeOptions = useMemo( + () => [ + { value: 'received_date', label: 'Tanggal Terima' }, + { value: 'po_date', label: 'Tanggal PO' }, + ], + [] + ); + + const sortByOptions = useMemo( + () => [ + { value: 'ASC', label: 'Ascending' }, + { value: 'DESC', label: 'Descending' }, + ], + [] + ); + + 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 supplierChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'supplier_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const productChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'product_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const productCategoryChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const arr = Array.isArray(val) ? val : val ? [val] : []; + updateFilter( + 'product_category_id', + arr.map((v) => String((v as OptionType).value)) + ); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const dataTypeChangeHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + const filterValue = + (newVal?.value as 'received_date' | 'po_date') || 'received_date'; + updateFilter('filter_by', filterValue); + updateFilter('received_date', ''); + updateFilter('po_date', ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const sortByHandler = useCallback( + (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + const sortValue = (newVal?.value as 'ASC' | 'DESC') || 'ASC'; + updateFilter('sort_by', sortValue); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const startDateChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('start_date', val || ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const endDateChangeHandler = useCallback< + ChangeEventHandler + >( + (e) => { + const val = e.target.value; + updateFilter('end_date', val || ''); + setIsSubmitted(false); + }, + [updateFilter] + ); + + const resetFilters = useCallback(() => { + updateFilter('area_id', []); + updateFilter('supplier_id', []); + updateFilter('product_id', []); + updateFilter('product_category_id', []); + updateFilter('received_date', ''); + updateFilter('po_date', ''); + updateFilter('start_date', ''); + updateFilter('end_date', ''); + updateFilter('sort_by', ''); + updateFilter('filter_by', 'received_date'); + setIsSubmitted(false); + }, [updateFilter]); + + const handleSubmit = useCallback(() => { + setIsSubmitted(true); + setCurrentPage(1); + }, []); + + // ===== DATA FETCHING ===== + const { data: purchasePerSupplier, isLoading } = useSWR( + isSubmitted + ? () => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id.join(',') + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id.join(',') + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id.join(',') + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + page: currentPage, + limit: pageSize, + }; + + return ['logistic-purchase-report', params]; + } + : null, + ([, params]) => + LogisticApi.getLogisticPurchasePerSupplierReport( + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date, + params.sort_by, + params.filter_by, + params.page, + params.limit + ) + ); + + const data: LogisticPurchasePerSupplierReport[] = useMemo( + () => + isResponseSuccess(purchasePerSupplier) + ? (purchasePerSupplier?.data as unknown as LogisticPurchasePerSupplierReport[]) || + [] + : [], + [purchasePerSupplier] + ); + + const meta = + isResponseSuccess(purchasePerSupplier) && purchasePerSupplier?.meta + ? purchasePerSupplier.meta + : null; + + // ===== EXPORT DATA FETCHER ===== + const logisticPurchasePerSupplierExport = useCallback(async (): Promise< + LogisticPurchasePerSupplierReport[] | null + > => { + const params = { + area_id: + tableFilterState.area_id.length > 0 + ? tableFilterState.area_id.join(',') + : undefined, + supplier_id: + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id.join(',') + : undefined, + product_id: + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id.join(',') + : undefined, + product_category_id: + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id.join(',') + : undefined, + received_date: + tableFilterState.filter_by === 'received_date' + ? tableFilterState.start_date || undefined + : undefined, + po_date: + tableFilterState.filter_by === 'po_date' + ? tableFilterState.start_date || undefined + : undefined, + start_date: tableFilterState.start_date || undefined, + end_date: tableFilterState.end_date || undefined, + sort_by: tableFilterState.sort_by || undefined, + filter_by: tableFilterState.filter_by || undefined, + limit: 100, + page: 1, + }; + + const response = await LogisticApi.getLogisticPurchasePerSupplierReport( + params.area_id, + params.supplier_id, + params.product_id, + params.product_category_id, + params.received_date, + params.po_date, + params.start_date, + params.end_date, + params.sort_by, + params.filter_by, + params.page, + params.limit + ); + + return isResponseSuccess(response) + ? (response.data as unknown as LogisticPurchasePerSupplierReport[]) + : null; + }, [tableFilterState]); + + // ===== EXPORT HANDLERS ===== + const handleExportExcel = useCallback(async () => { + setIsExcelExportLoading(true); + try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.length === 0 + ) { + toast.error('Tidak ada data untuk diekspor.'); + return; + } + + const workbook = XLSX.utils.book_new(); + + allDataForExport.forEach((supplierReport) => { + const supplierData = supplierReport.rows; + const supplierName = + supplierReport.supplier?.name || 'Unknown Supplier'; + + const excelData: { [key: string]: string | number }[] = + supplierData.map((item, index) => ({ + No: index + 1, + 'Tanggal Terima': item.receive_date + ? formatDate(item.receive_date, 'DD MMM YYYY') + : '', + 'Tanggal PO': item.po_date + ? formatDate(item.po_date, 'DD MMM YYYY') + : '', + 'No. Referensi': item.po_number || '', + 'Nama Produk': item.product?.name || '', + Tujuan: item.warehouse?.name || '', + QTY: item.qty || 0, + 'Harga Beli (Rp)': item.unit_price || 0, + 'Value Harga Beli (Rp)': item.purchase_value || 0, + 'Transport (Rp)': item.transport_unit_price || 0, + 'Value Transport (Rp)': item.transport_value || 0, + 'Jumlah (Rp)': item.total_amount || 0, + Ekspedisi: item.expedition || '', + 'Surat Jalan': item.delivery_number || '', + })); + + if (supplierReport.summary) { + excelData.push({ + No: 'Total', + 'Tanggal Terima': '', + 'Tanggal PO': '', + 'No. Referensi': '', + 'Nama Produk': '', + Tujuan: '', + QTY: supplierReport.summary.total_qty || 0, + 'Harga Beli (Rp)': '', + 'Value Harga Beli (Rp)': + supplierReport.summary.total_purchase_value || 0, + 'Transport (Rp)': '', + 'Value Transport (Rp)': + supplierReport.summary.total_transport_value || 0, + 'Jumlah (Rp)': supplierReport.summary.total_amount || 0, + Ekspedisi: '', + 'Surat Jalan': '', + }); + } + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 5 }, // No + { wch: 15 }, // Tanggal Terima + { wch: 15 }, // Tanggal PO + { wch: 15 }, // No. Referensi + { wch: 30 }, // Nama Produk + { wch: 20 }, // Tujuan + { wch: 10 }, // QTY + { wch: 18 }, // Harga Beli + { wch: 20 }, // Value Harga Beli + { wch: 15 }, // Transport + { wch: 20 }, // Value Transport + { wch: 18 }, // Jumlah + { wch: 15 }, // Ekspedisi + { wch: 15 }, // Surat Jalan + ]; + worksheet['!cols'] = colWidths; + + const sheetName = + supplierName.length > 31 + ? supplierName.substring(0, 31) + : supplierName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + }); + + const filename = `laporan-pembelian-per-supplier-dicetak-pada-${formatDate(new Date(), 'YYYY-MM-DD-HHmm')}.xlsx`; + + XLSX.writeFile(workbook, filename); + toast.success('Excel berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat Excel. Silakan coba lagi.'); + } finally { + setIsExcelExportLoading(false); + } + }, [ + logisticPurchasePerSupplierExport, + tableFilterState, + areaOptions, + supplierOptions, + ]); + + const handleExportPdf = useCallback(async () => { + setIsPdfExportLoading(true); + try { + const allDataForExport = await logisticPurchasePerSupplierExport(); + + if ( + !allDataForExport || + !Array.isArray(allDataForExport) || + allDataForExport.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 supplierName = + tableFilterState.supplier_id.length > 0 + ? tableFilterState.supplier_id + .map( + (id) => + supplierOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Supplier' + : 'Semua Supplier'; + + const productName = + tableFilterState.product_id.length > 0 + ? tableFilterState.product_id + .map( + (id) => + productOptions.find((opt) => opt.value === Number(id))?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Produk' + : 'Semua Produk'; + + const productCategoryName = + tableFilterState.product_category_id.length > 0 + ? tableFilterState.product_category_id + .map( + (id) => + productCategoryOptions.find((opt) => opt.value === Number(id)) + ?.label + ) + .filter(Boolean) + .join(', ') || 'Semua Kategori Produk' + : 'Semua Kategori Produk'; + + const exportParams = { + area_name: areaName, + supplier_name: supplierName, + product_name: productName, + product_category_name: productCategoryName, + filter_by: tableFilterState.filter_by || 'received_date', + start_date: tableFilterState.start_date || '', + end_date: tableFilterState.end_date || '', + }; + + await generatePurchasesPerSupplierPDF(allDataForExport, exportParams); + toast.success('PDF berhasil dibuat dan diunduh.'); + } catch { + toast.error('Gagal membuat PDF. Silakan coba lagi.'); + } finally { + setIsPdfExportLoading(false); + } + }, [ + logisticPurchasePerSupplierExport, + tableFilterState, + areaOptions, + supplierOptions, + productOptions, + productCategoryOptions, + ]); + + // ===== 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); + } + }; + + const getTableColumns = ( + summary: LogisticPurchasePerSupplierSummary + ): ColumnDef[] => { + const tableColumns: ColumnDef< + LogisticPurchasePerSupplierReport['rows'][0] + >[] = [ + { + id: 'no', + header: 'No', + cell: (props) => props.row.index + 1, + footer: () =>
    Total
    , + }, + + { + id: 'received_date', + header: 'Tanggal Terima', + accessorKey: 'receive_date', + cell: (props) => { + const value = props.row.original.receive_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'po_date', + header: 'Tanggal PO', + accessorKey: 'po_date', + cell: (props) => { + const value = props.row.original.po_date; + return formatDate(value, 'DD MMM YYYY'); + }, + }, + { + id: 'po_number', + header: 'No. Referensi', + accessorKey: 'po_number', + cell: (props) => { + const value = props.row.original.po_number; + return value || '-'; + }, + }, + { + id: 'product_name', + header: 'Nama Produk', + accessorKey: 'product.name', + cell: (props) => { + const product = props.row.original.product; + return product?.name || '-'; + }, + }, + { + id: 'destination_warehouse', + header: 'Tujuan', + accessorKey: 'warehouse.name', + cell: (props) => { + const warehouse = props.row.original.warehouse; + return warehouse?.name || '-'; + }, + }, + { + id: 'qty', + header: 'QTY', + accessorKey: 'qty', + cell: (props) => { + const value = props.row.original.qty; + return
    {formatNumber(value)}
    ; + }, + footer: () => ( +
    + {formatNumber(summary.total_qty) || '-'} +
    + ), + }, + { + id: 'price', + header: 'Harga Beli (Rp)', + accessorKey: 'unit_price', + cell: (props) => { + const value = props.row.original.unit_price; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summary.total_unit_price) || '-'} +
    + ), + }, + { + id: 'purchase_amount', + header: 'Value Harga Beli (Rp)', + accessorKey: 'purchase_value', + cell: (props) => { + const value = props.row.original.purchase_value; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summary.total_purchase_value) || '-'} +
    + ), + }, + { + id: 'transport', + header: 'Transport (Rp)', + accessorKey: 'transport_unit_price', + cell: (props) => { + const value = props.row.original.transport_unit_price; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summary.total_transport_unit_price) || '-'} +
    + ), + }, + { + id: 'value_transport', + header: 'Value Transport (Rp)', + accessorKey: 'transport_value', + cell: (props) => { + const value = props.row.original.transport_value; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summary.total_transport_value) || '-'} +
    + ), + }, + { + id: 'total', + header: 'Jumlah (Rp)', + accessorKey: 'total_amount', + cell: (props) => { + const value = props.row.original.total_amount; + return
    {formatCurrency(value)}
    ; + }, + footer: () => ( +
    + {formatCurrency(summary.total_amount) || '-'} +
    + ), + }, + { + id: 'expedition_vendor_name', + header: 'Ekspedisi', + accessorKey: 'expedition', + cell: (props) => { + const value = props.row.original.expedition; + return value || '-'; + }, + }, + { + id: 'travel_number', + header: 'Surat Jalan', + accessorKey: 'delivery_number', + cell: (props) => { + const value = props.row.original.delivery_number; + return value || '-'; + }, + }, + ]; + return tableColumns; + }; + + return ( +
    + +
    + + + + Export + + } + align='end' + > + + + + + +
    +
    + + (tableFilterState.area_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={areaChangeHandler} + isLoading={isLoadingAreas} + isClearable + /> + + (tableFilterState.supplier_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={supplierChangeHandler} + isLoading={isLoadingSuppliers} + isClearable + /> + + (tableFilterState.product_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={productChangeHandler} + isLoading={isLoadingProducts} + isClearable + /> +
    +
    + + (tableFilterState.product_category_id || []) + .map(String) + .includes(String(opt.value)) + )} + onChange={productCategoryChangeHandler} + isLoading={isLoadingProductCategories} + isClearable + /> +
    + option.value === tableFilterState.filter_by + ) || null + } + onChange={dataTypeChangeHandler} + isLoading={false} + isClearable={false} + /> + option.value === tableFilterState.sort_by + ) || null + } + onChange={sortByHandler} + isLoading={false} + isClearable={false} + /> +
    +
    + + +
    +
    + + {!isSubmitted ? ( +
    + Silakan pilih filter dan klik tombol Submit untuk menampilkan data. +
    + ) : isLoading ? ( +
    + +
    + ) : data.length === 0 ? ( +
    + Tidak ada data yang dapat ditampilkan... +
    + ) : ( + data.map((supplierReport) => { + const summary = supplierReport.summary || { + total_qty: 0, + total_unit_price: 0, + total_purchase_value: 0, + total_transport_unit_price: 0, + total_transport_value: 0, + total_amount: 0, + }; + + const totalPurchase = summary.total_amount; + const tableColumns = getTableColumns(summary); + + return ( + +
    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 PurchasesPerSupplierTab; 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) => ( + + ))} + + ); + + const customRows = [ + + + , + ]; + + if (perWeightRangeSummary.length > 0) { + perWeightRangeSummary.forEach( + (item: HppPerKandangPerWeightRange, index = 0) => { + customRows.push( + + + + + + + + + + + + + + + + + + ); + } + ); + } + + 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... +
    + ) : ( +
    + {flexRender(cell.column.columnDef.cell, cell.getContext())} +
    + Rekapitulasi per rentang bobot +
    {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)} +
    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/config/constant.ts b/src/config/constant.ts index 70308901..47352d8b 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -10,14 +10,20 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Produksi', link: '/production', icon: 'heroicons-outline:wrench-screwdriver', + permission: [ + 'lti.production.project_flocks.list', + 'lti.production.recording.list', + ], submenu: [ { text: 'Daftar Flock', link: '/production/project-flock', + permission: ['lti.production.project_flocks.list'], }, { text: 'Recording', link: '/production/recording', + permission: ['lti.production.recording.list'], }, { text: 'Transfer to Laying', @@ -29,6 +35,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Pembelian', link: '/purchase', icon: 'heroicons-outline:shopping-cart', + permission: ['lti.purchase.list'], }, { text: 'Penjualan', @@ -41,14 +48,16 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ icon: 'heroicons-outline:banknotes', }, { - text: 'Biaya Operasional', + text: 'Biaya', link: '/expense', icon: 'heroicons:wallet', + permission: ['lti.expense.list'], }, { text: 'Closing', link: '/closing', icon: 'heroicons-outline:presentation-chart-bar', + permission: ['lti.closing.list'], }, { text: 'Laporan', @@ -63,24 +72,36 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Biaya Operasional', link: '/report/expense', }, + { + text: 'Penjualan', + link: '/report/marketing', + }, ], }, { text: 'Persediaan', link: '/inventory', icon: 'heroicons-outline:folder', + permission: [ + 'lti.inventory.product_stock.list', + 'lti.inventory.product_warehouses.list', + 'lti.inventory.transfer.list', + ], submenu: [ { text: 'Stok Produk', link: '/inventory/product', + permission: ['lti.inventory.product_stock.list'], }, { text: 'Penyesuaian Stok', link: '/inventory/adjustment', + permission: ['lti.inventory.product_stock.list'], }, { text: 'Transfer Stok', link: '/inventory/movement', + permission: ['lti.inventory.transfer.list'], }, ], }, @@ -88,58 +109,86 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Master Data', link: '/master-data', icon: 'heroicons-outline:circle-stack', + permission: [ + 'lti.master.area.list', + 'lti.master.banks.list', + 'lti.master.customer.list', + 'lti.master.fcr.list', + 'lti.master.flocks.list', + 'lti.master.kandangs.list', + 'lti.master.locations.list', + 'lti.master.nonstocks.list', + 'lti.master.product_categories.list', + 'lti.master.products.list', + 'lti.master.suppliers.list', + 'lti.master.uoms.list', + 'lti.master.warehouses.list', + ], submenu: [ { text: 'Produk', link: '/master-data/product', + permission: ['lti.master.products.list'], }, { text: 'Kategori Produk', link: '/master-data/product-category', + permission: ['lti.master.product_categories.list'], }, { text: 'Bank', link: '/master-data/bank', + permission: ['lti.master.banks.list'], }, { text: 'Area', link: '/master-data/area', + permission: ['lti.master.area.list'], }, { text: 'Lokasi', link: '/master-data/location', + permission: ['lti.master.locations.list'], }, { text: 'Kandang', link: '/master-data/kandang', + permission: ['lti.master.kandangs.list'], }, { text: 'Warehouse', link: '/master-data/warehouse', + permission: ['lti.master.warehouses.list'], }, { text: 'Customer', link: '/master-data/customer', + permission: ['lti.master.customer.list'], }, { text: 'UOM', link: '/master-data/uom', + permission: ['lti.master.uoms.list'], }, { text: 'Non-Stock', link: '/master-data/nonstock', + permission: ['lti.master.nonstocks.list'], }, { text: 'FCR', link: '/master-data/fcr', + permission: ['lti.master.fcr.list'], }, { text: 'Supplier', link: '/master-data/supplier', + permission: ['lti.master.suppliers.list'], }, { text: 'Flock', link: '/master-data/flock', + permission: ['lti.master.flocks.list'], }, { text: 'Standar Produksi', @@ -288,3 +337,29 @@ export const ACCEPTED_FILE_TYPE = { 'image/*': [], }, }; + +export const FILTER_TYPE_OPTIONS = [ + { + label: 'Tanggal Realisasi', + value: 'REALIZATION_DATE', + }, + { + label: 'Tanggal DO', + value: 'DO_DATE', + }, +]; + +export const MARKETING_TYPE_OPTIONS = [ + { + label: 'Ayam', + value: 'ayam', + }, + { + label: 'Telur', + value: 'telur', + }, + { + label: 'Trading', + value: 'trading', + }, +]; diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts new file mode 100644 index 00000000..ed6d4771 --- /dev/null +++ b/src/config/route-permission.ts @@ -0,0 +1,155 @@ +export const ROUTE_PERMISSIONS: Record = { + '/': ['lti.dashboard.list'], + + // Dashboard + '/dashboard/': ['lti.dashboard.list'], + + // Production + // Production - Project Flock + '/production/project-flock/': ['lti.production.project_flocks.list'], + '/production/project-flock/add/': ['lti.production.project_flocks.create'], + '/production/project-flock/detail/': ['lti.production.project_flocks.detail'], + '/production/project-flock/detail/edit/': [ + 'lti.production.project_flocks.update', + ], + '/production/project-flock/chickin/add/kandang/': [ + 'lti.production.chickins.create', + ], + '/production/project-flock/closing/': [ + 'lti.production.project_flock_kandangs.closing', + ], + + // Production - Recording + '/production/recording/': ['lti.production.recording.list'], + '/production/recording/add/': ['lti.production.recording.create'], + '/production/recording/detail/': ['lti.production.recording.detail'], + '/production/recording/detail/edit/': ['lti.production.recording.update'], + + // Production - Transfer to Laying + '/production/transfer-to-laying/': ['lti.production.transfer_to_laying.list'], + '/production/transfer-to-laying/add/': [ + 'lti.production.transfer_to_laying.create', + ], + '/production/transfer-to-laying/detail/': [ + 'lti.production.transfer_to_laying.detail', + ], + '/production/transfer-to-laying/detail/edit/': [ + 'lti.production.transfer_to_laying.update', + ], + + // Purchase + '/purchase/': ['lti.purchase.list'], + '/purchase/add/': ['lti.purchase.create'], + '/purchase/detail/': ['lti.purchase.detail'], + '/purchase/detail/edit/': ['lti.purchase.update'], + + // Marketing + '/marketing/': ['lti.marketing.delivery_order.list'], + '/marketing/add/delivery-orders/': ['lti.marketing.delivery_order.create'], + '/marketing/add/sales-orders/': ['lti.marketing.sales_order.create'], + '/marketing/detail/': ['lti.marketing.delivery_order.detail'], + '/marketing/detail/delivery-orders/edit/': [ + 'lti.marketing.delivery_order.update', + ], + '/marketing/detail/sales-orders/edit/': ['lti.marketing.sales_order.update'], + + // Expense + '/expense/': ['lti.expense.list'], + '/expense/add/': ['lti.expense.create'], + '/expense/detail/': ['lti.expense.detail'], + '/expense/detail/edit/': ['lti.expense.update'], + '/expense/realization/': ['lti.expense.create.realization'], + '/expense/realization/edit/': ['lti.expense.update.realization'], + + // Closing + '/closing/': ['lti.closing.list'], + '/closing/detail/': ['lti.closing.detail'], + + // Report + '/report/logistic-stock/': ['lti.repport.purchasesupplier.list'], + '/report/expense/': ['lti.repport.expense.list'], + '/report/marketing/': ['lti.repport.delivery.list'], + + // Inventory + '/inventory/adjustment/': ['lti.inventory.list'], + '/inventory/adjustment/add/': ['lti.inventory.create'], + '/inventory/adjustment/detail/': ['lti.inventory.detail'], + '/inventory/movement/': ['lti.inventory.transfer.list'], + '/inventory/movement/add/': ['lti.inventory.transfer.create'], + '/inventory/movement/detail/': ['lti.inventory.transfer.detail'], + '/inventory/movement/detail/edit/': ['lti.inventory.transfer.update'], + '/inventory/product/': ['lti.inventory.product_stock.list'], + '/inventory/product/detail/': ['lti.inventory.product_stock.detail'], + + // Master Data + '/master-data/product/': ['lti.master.products.list'], + '/master-data/product/add/': ['lti.master.products.create'], + '/master-data/product/detail/': ['lti.master.products.detail'], + '/master-data/product/detail/edit/': ['lti.master.products.update'], + + '/master-data/product-category/': ['lti.master.product_categories.list'], + '/master-data/product-category/add/': [ + 'lti.master.product_categories.create', + ], + '/master-data/product-category/detail/': [ + 'lti.master.product_categories.detail', + ], + '/master-data/product-category/detail/edit/': [ + 'lti.master.product_categories.update', + ], + + '/master-data/bank/': ['lti.master.banks.list'], + '/master-data/bank/add/': ['lti.master.banks.create'], + '/master-data/bank/detail/': ['lti.master.banks.detail'], + '/master-data/bank/detail/edit/': ['lti.master.banks.update'], + + '/master-data/area/': ['lti.master.area.list'], + '/master-data/area/add/': ['lti.master.area.create'], + '/master-data/area/detail/': ['lti.master.area.detail'], + '/master-data/area/detail/edit/': ['lti.master.area.update'], + + '/master-data/location/': ['lti.master.locations.list'], + '/master-data/location/add/': ['lti.master.locations.create'], + '/master-data/location/detail/': ['lti.master.locations.detail'], + '/master-data/location/detail/edit/': ['lti.master.locations.update'], + + '/master-data/kandang/': ['lti.master.kandangs.list'], + '/master-data/kandang/add/': ['lti.master.kandangs.create'], + '/master-data/kandang/detail/': ['lti.master.kandangs.detail'], + '/master-data/kandang/detail/edit/': ['lti.master.kandangs.update'], + + '/master-data/warehouse/': ['lti.master.warehouses.list'], + '/master-data/warehouse/add/': ['lti.master.warehouses.create'], + '/master-data/warehouse/detail/': ['lti.master.warehouses.detail'], + '/master-data/warehouse/detail/edit/': ['lti.master.warehouses.update'], + + '/master-data/customer/': ['lti.master.customer.list'], + '/master-data/customer/add/': ['lti.master.customer.create'], + '/master-data/customer/detail/': ['lti.master.customer.detail'], + '/master-data/customer/detail/edit/': ['lti.master.customer.update'], + + '/master-data/uom/': ['lti.master.uoms.list'], + '/master-data/uom/add/': ['lti.master.uoms.create'], + '/master-data/uom/detail/': ['lti.master.uoms.detail'], + '/master-data/uom/detail/edit/': ['lti.master.uoms.update'], + + '/master-data/nonstock/': ['lti.master.nonstocks.list'], + '/master-data/nonstock/add/': ['lti.master.nonstocks.create'], + '/master-data/nonstock/detail/': ['lti.master.nonstocks.detail'], + '/master-data/nonstock/detail/edit/': ['lti.master.nonstocks.update'], + + '/master-data/fcr/': ['lti.master.fcr.list'], + '/master-data/fcr/add/': ['lti.master.fcr.create'], + '/master-data/fcr/detail/': ['lti.master.fcr.detail'], + '/master-data/fcr/detail/edit/': ['lti.master.fcr.update'], + + '/master-data/supplier/': ['lti.master.suppliers.list'], + '/master-data/supplier/add/': ['lti.master.suppliers.create'], + '/master-data/supplier/detail/': ['lti.master.suppliers.detail'], + '/master-data/supplier/detail/edit/': ['lti.master.suppliers.update'], + + '/master-data/flock/': ['lti.master.flocks.list'], + '/master-data/flock/add/': ['lti.master.flocks.create'], + '/master-data/flock/detail/': ['lti.master.flocks.detail'], + '/master-data/flock/detail/edit/': ['lti.master.flocks.update'], +}; diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..3b9a9a7b --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,1175 @@ +/** + * Dummy Data untuk Closing API + * + * File ini berisi dummy data untuk testing API Closing sebelum backend siap. + * + * Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts + * + * @example + * // 1. Menggunakan getAllFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * '/closings', + * ClosingApi.getAllFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of Closing objects + * } + * + * @example + * // 2. Menggunakan getSingle: + * import { ClosingApi } from '@/services/api/closing'; + * + * const response = await ClosingApi.getSingle(1); + * if (response?.status === 'success') { + * console.log(response.data); // Single Closing object + * } else if (response?.status === 'error') { + * console.error(response.message); // Error message + * } + * + * @example + * // 3. Menggunakan getGeneralInfo dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const closingId = 1; + * const { data, error, isLoading } = useSWR( + * closingId, + * (id: number) => ClosingApi.getGeneralInfo(id) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // ClosingGeneralInformation object + * } + * + * @example + * // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/incoming`, + * ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingIncomingSapronak + * } + * + * @example + * // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/outgoing`, + * ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingOutgoingSapronak + * } + * + * @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts} + */ + +import { format } from 'date-fns'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingOverhead, + ClosingProductionData, + ClosingSapronakCalculation, +} from '@/types/api/closing'; +import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; + +// Waktu saat ini untuk created_at/updated_at +const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); +const today = format(new Date(), 'yyyy-MM-dd'); +const yesterday = format( + new Date().setDate(new Date().getDate() - 1), + 'yyyy-MM-dd' +); +const lastWeek = format( + new Date().setDate(new Date().getDate() - 7), + 'yyyy-MM-dd' +); +const lastMonth = format( + new Date().setMonth(new Date().getMonth() - 1), + 'yyyy-MM-dd' +); + +// ====================== +// 👤 Created User +// ====================== +export const createdUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin Utama', +}; + +// ====================== +// 📊 Closing Dummy Data +// ====================== +export const dummyClosings: Closing[] = [ + // 1. Closing dengan status Pengajuan - GROWING + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. Closing dengan status Aktif - LAYING + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. Closing dengan status Selesai - GROWING + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. Closing dengan status Aktif - LAYING + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. Closing dengan status Selesai - GROWING + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. Closing dengan status Pengajuan - LAYING + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, // Belum ada tanggal closing + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. Closing dengan status Aktif - GROWING + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. Closing dengan status Selesai - LAYING + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📊 Closing General Information Dummy Data +// ====================== +export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [ + // 1. General Info - GROWING - Pengajuan + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + flock_id: 101, + project_type: 'GROWING', + population: 15000, + active_house_count: 3, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. General Info - LAYING - Aktif + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + flock_id: 102, + project_type: 'LAYING', + population: 10000, + active_house_count: 2, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. General Info - GROWING - Selesai + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + flock_id: 103, + project_type: 'GROWING', + population: 20000, + active_house_count: 4, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. General Info - LAYING - Aktif + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + flock_id: 104, + project_type: 'LAYING', + population: 5000, + active_house_count: 1, + closing_status: 'In Progress', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. General Info - GROWING - Selesai + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 105, + project_type: 'GROWING', + population: 25000, + active_house_count: 5, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. General Info - LAYING - Pengajuan + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + flock_id: 106, + project_type: 'LAYING', + population: 12000, + active_house_count: 2, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. General Info - GROWING - Aktif + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + flock_id: 107, + project_type: 'GROWING', + population: 18000, + active_house_count: 3, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. General Info - LAYING - Selesai + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 108, + project_type: 'LAYING', + population: 30000, + active_house_count: 6, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📦 Incoming Sapronak Dummy Data +// ====================== +export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'IN-2025-001', + transaction_type: 'Pembelian', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang A1', + quantity: 5000, + unit: 'Ekor', + formatted_quantity: '5,000 Ekor', + notes: 'DOC berkualitas tinggi dari supplier terpercaya', + }, + { + id: 2, + date: yesterday, + reference_number: 'IN-2025-002', + transaction_type: 'Transfer Masuk', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Gudang Area Bandung', + destination_warehouse: 'Kandang B1', + quantity: 100, + unit: 'Sak', + formatted_quantity: '100 Sak (5,000 Kg)', + notes: 'Pakan starter untuk periode awal', + }, + { + id: 3, + date: lastWeek, + reference_number: 'IN-2025-003', + transaction_type: 'Pembelian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Supplier Medion', + destination_warehouse: 'Gudang Farmasi', + quantity: 50, + unit: 'Botol', + formatted_quantity: '50 Botol', + notes: 'Vitamin untuk meningkatkan daya tahan tubuh', + }, + { + id: 4, + date: today, + reference_number: 'IN-2025-004', + transaction_type: 'Pembelian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang C1', + quantity: 200, + unit: 'Sak', + formatted_quantity: '200 Sak (10,000 Kg)', + notes: 'Pakan finisher untuk periode akhir', + }, + { + id: 5, + date: yesterday, + reference_number: 'IN-2025-005', + transaction_type: 'Transfer Masuk', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Area Jakarta', + destination_warehouse: 'Gudang Farmasi', + quantity: 30, + unit: 'Box', + formatted_quantity: '30 Box', + notes: 'Antibiotik untuk pencegahan penyakit', + }, +]; + +// ====================== +// 📤 Outgoing Sapronak Dummy Data +// ====================== +export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'OUT-2025-001', + transaction_type: 'Pemakaian', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang A1', + destination_warehouse: 'Konsumsi Kandang A1', + quantity: 50, + unit: 'Sak', + formatted_quantity: '50 Sak (2,500 Kg)', + notes: 'Pemakaian pakan harian periode starter', + }, + { + id: 2, + date: yesterday, + reference_number: 'OUT-2025-002', + transaction_type: 'Transfer Keluar', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Kandang B1', + destination_warehouse: 'Kandang B2', + quantity: 1000, + unit: 'Ekor', + formatted_quantity: '1,000 Ekor', + notes: 'Transfer DOC ke kandang baru', + }, + { + id: 3, + date: lastWeek, + reference_number: 'OUT-2025-003', + transaction_type: 'Pemakaian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 10, + unit: 'Botol', + formatted_quantity: '10 Botol', + notes: 'Pemberian vitamin untuk meningkatkan kesehatan', + }, + { + id: 4, + date: today, + reference_number: 'OUT-2025-004', + transaction_type: 'Pemakaian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Kandang C1', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 80, + unit: 'Sak', + formatted_quantity: '80 Sak (4,000 Kg)', + notes: 'Pemakaian pakan harian periode finisher', + }, + { + id: 5, + date: yesterday, + reference_number: 'OUT-2025-005', + transaction_type: 'Pemakaian', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang D1', + quantity: 5, + unit: 'Box', + formatted_quantity: '5 Box', + notes: 'Pengobatan untuk ayam yang sakit', + }, + { + id: 6, + date: lastWeek, + reference_number: 'OUT-2025-006', + transaction_type: 'Transfer Keluar', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang E1', + destination_warehouse: 'Kandang E2', + quantity: 30, + unit: 'Sak', + formatted_quantity: '30 Sak (1,500 Kg)', + notes: 'Transfer pakan antar kandang', + }, +]; + +// ====================== +// 📊 Perhitungan Sapronak Dummy Data +// ====================== +export const dummySapronakCalculation: ClosingSapronakCalculation = { + // DOC Broiler Calculation + doc_broiler: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-001', + qty_masuk: 5000, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 40000000, + keterangan: 'Pembelian DOC dari supplier', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'OUT-2025-002', + qty_masuk: 0, + qty_keluar: 1000, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 8000000, + keterangan: 'Transfer DOC ke kandang lain', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'USE-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 400000, + keterangan: 'Mortalitas DOC', + }, + ], + total: { + label: 'Total DOC Broiler', + qty_masuk: 5000, + qty_keluar: 1000, + qty_pakai: 50, + harga_beli_per_qty: 8000, + total_harga: 48400000, + }, + }, + + // OVK Calculation + ovk: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-003', + qty_masuk: 50, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 7500000, + keterangan: 'Pembelian vitamin', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'IN-2025-005', + qty_masuk: 30, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 7500000, + keterangan: 'Pembelian antibiotik', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'OUT-2025-003', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 10, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 1500000, + keterangan: 'Pemakaian vitamin', + }, + { + id: 4, + tanggal: yesterday, + no_referensi: 'OUT-2025-005', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 5, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 1250000, + keterangan: 'Pemakaian antibiotik', + }, + ], + total: { + label: 'Total OVK', + qty_masuk: 80, + qty_keluar: 0, + qty_pakai: 15, + harga_beli_per_qty: 200000, + total_harga: 17750000, + }, + }, + + // Pakan Calculation + pakan: { + rows: [ + { + id: 1, + tanggal: yesterday, + no_referensi: 'IN-2025-002', + qty_masuk: 100, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 45000000, + keterangan: 'Pembelian pakan starter', + }, + { + id: 2, + tanggal: today, + no_referensi: 'IN-2025-004', + qty_masuk: 200, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 96000000, + keterangan: 'Pembelian pakan finisher', + }, + { + id: 3, + tanggal: today, + no_referensi: 'OUT-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 22500000, + keterangan: 'Pemakaian pakan starter', + }, + { + id: 4, + tanggal: today, + no_referensi: 'OUT-2025-004', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 80, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 38400000, + keterangan: 'Pemakaian pakan finisher', + }, + { + id: 5, + tanggal: lastWeek, + no_referensi: 'OUT-2025-006', + qty_masuk: 0, + qty_keluar: 30, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 13500000, + keterangan: 'Transfer pakan ke kandang lain', + }, + ], + total: { + label: 'Total Pakan', + qty_masuk: 300, + qty_keluar: 30, + qty_pakai: 130, + harga_beli_per_qty: 465000, + total_harga: 215400000, + }, + }, +}; + +// ====================== +// 💰 Overhead Dummy Data +// ====================== +export const dummyOverhead: ClosingOverhead = { + overheads: [ + { + item_name: 'Expedisi DOC', + uom_name: 'Ekor', + budget_quantity: 500, + budget_unit_price: 8000, + budget_total_amount: 4000000, + actual_date: '', + actual_quantity: 0, + actual_unit_price: 0, + actual_total_amount: 0, + cost_per_bird: 0, + }, + { + item_name: 'Solar', + uom_name: 'Liter', + budget_quantity: 0, + budget_unit_price: 0, + budget_total_amount: 0, + actual_date: today, + actual_quantity: 20, + actual_unit_price: 10000, + actual_total_amount: 200000, + cost_per_bird: 200, + }, + { + item_name: 'Gaji Karyawan Kandang', + uom_name: 'Orang', + budget_quantity: 3, + budget_unit_price: 3000000, + budget_total_amount: 9000000, + actual_date: today, + actual_quantity: 3, + actual_unit_price: 3200000, + actual_total_amount: 9600000, + cost_per_bird: 640, + }, + { + item_name: 'Listrik Kandang', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 2500000, + budget_total_amount: 2500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 2800000, + actual_total_amount: 2800000, + cost_per_bird: 187, + }, + { + item_name: 'Air Bersih', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 500000, + budget_total_amount: 500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 450000, + actual_total_amount: 450000, + cost_per_bird: 30, + }, + { + item_name: 'Perbaikan Kandang', + uom_name: 'Paket', + budget_quantity: 1, + budget_unit_price: 3000000, + budget_total_amount: 3000000, + actual_date: yesterday, + actual_quantity: 1, + actual_unit_price: 3500000, + actual_total_amount: 3500000, + cost_per_bird: 233, + }, + { + item_name: 'Service Peralatan', + uom_name: 'Kali', + budget_quantity: 2, + budget_unit_price: 500000, + budget_total_amount: 1000000, + actual_date: lastWeek, + actual_quantity: 2, + actual_unit_price: 550000, + actual_total_amount: 1100000, + cost_per_bird: 73, + }, + { + item_name: 'ATK & Supplies', + uom_name: 'Paket', + budget_quantity: 1, + budget_unit_price: 500000, + budget_total_amount: 500000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 450000, + actual_total_amount: 450000, + cost_per_bird: 30, + }, + { + item_name: 'Biaya Komunikasi', + uom_name: 'Bulan', + budget_quantity: 1, + budget_unit_price: 300000, + budget_total_amount: 300000, + actual_date: today, + actual_quantity: 1, + actual_unit_price: 320000, + actual_total_amount: 320000, + cost_per_bird: 21, + }, + { + item_name: 'BBM Kendaraan Operasional', + uom_name: 'Liter', + budget_quantity: 200, + budget_unit_price: 10000, + budget_total_amount: 2000000, + actual_date: today, + actual_quantity: 220, + actual_unit_price: 10500, + actual_total_amount: 2310000, + cost_per_bird: 154, + }, + ], + total: { + budget_quantity: 710, + budget_total_amount: 23300000, + actual_quantity: 250, + actual_total_amount: 24530000, + cost_per_bird: 1568, + }, +}; + +// ====================== +// 🔧 Dummy API Response Functions +// ====================== + +/** + * Dummy implementation for getAllFetcher + * Returns all closing records + */ +export const dummyGetAllFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: Closing[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: dummyClosings, + }; +}; + +/** + * Dummy implementation for getSingle + * Returns a single closing by ID + */ +export const dummyGetSingle = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closing = dummyClosings.find((c) => c.id === id); + + if (!closing) { + return { + code: 404, + status: 'error', + message: `Closing dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: closing, + }; +}; + +/** + * Dummy implementation for getAllIncomingSapronakFetcher + * Returns all incoming sapronak records + */ +export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingIncomingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak masuk berhasil diambil', + data: dummyIncomingSapronaks, + }; +}; + +/** + * Dummy implementation for getAllOutgoingSapronakFetcher + * Returns all outgoing sapronak records + */ +export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingOutgoingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak keluar berhasil diambil', + data: dummyOutgoingSapronaks, + }; +}; + +/** + * Dummy implementation for getGeneralInfo + * Returns closing general information by ID + */ +export const dummyGetGeneralInfo = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id); + + if (!closingInfo) { + return { + code: 404, + status: 'error', + message: `Closing general information dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing general information berhasil diambil', + data: closingInfo, + }; +}; + +/** + * Dummy implementation for getPerhitunganSapronak + * Returns sapronak calculation data + */ +export const dummyGetPerhitunganSapronak = async ( + id: number +): Promise< + | { + code: number; + status: 'success'; + message: string; + data: ClosingSapronakCalculation; + } + | undefined +> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data perhitungan sapronak berhasil diambil', + data: dummySapronakCalculation, + }; +}; + +/** + * Dummy implementation for getOverhead + * Returns overhead data + */ +export const dummyGetOverhead = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data overhead berhasil diambil', + data: dummyOverhead, + }; +}; + +export const dummyClosingProductionData: ClosingProductionData = { + purchase: { + initial_population: 12000, + claim_culling: 150, + final_population: 11850, + feed_in: 24000, + feed_used: 22500, + feed_used_per_head: 1.9, + }, + + sales: { + chicken: { + sales_population: 10500, + sales_weight: 21000, + average_weight: 2.0, + chicken_average_selling_price: 28500, + }, + egg: { + egg_pieces: 185000, + egg_mass_kg: 9250, + average_egg_weight_kg: 0.05, + egg_average_selling_price: 1800, + }, + }, + + performance: { + depletion: 150, + age_day: 35, + mortality_std: 3.5, + mortality_act: 4.2, + deff_mortality: 0.7, + fcr_std: 1.6, + fcr_act: 1.72, + deff_fcr: 0.12, + awg: 60, + }, +}; diff --git a/src/dummy/report/marketing-report.dummy.ts b/src/dummy/report/marketing-report.dummy.ts new file mode 100644 index 00000000..ea5af398 --- /dev/null +++ b/src/dummy/report/marketing-report.dummy.ts @@ -0,0 +1,139 @@ +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; + +// TODO: delete this later +export const DAILY_MARKETING_DUMMY_DATA: BaseApiResponse = + { + code: 200, + status: 'success', + message: 'Get daily marketing report successfully', + meta: { + page: 1, + limit: 10, + total_pages: 1, + total_results: 2, + }, + data: { + rows: [ + { + // metadata + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-12-01T08:00:00Z', + updated_at: '2025-12-01T08:00:00Z', + + // row data + no: 1, + so_date: '2025-12-01', + do_date: '2025-12-08', + aging_days: 7, + + warehouse: { + id: 1, + name: 'Warehouse Kandang A', + type: 'KANDANG', + area: { + id: 1, + name: 'Area Barat', + }, + location: { + id: 1, + name: 'Farm Bandung', + address: 'Jl. Raya Farm No. 1', + area: null, + }, + kandang: { + id: 1, + name: 'Kandang A1', + status: 'ACTIVE', + capacity: 5000, + location: null, + pic: null, + }, + }, + + customer: { + id: 1, + name: 'PT Maju Jaya', + pic_id: 10, + pic: { + id: 10, + id_user: 210, + email: 'pic@majujaya.com', + name: 'Budi Santoso', + }, + type: 'BROILER', + address: 'Jl. Industri No. 10', + phone: '08123456789', + email: 'contact@majujaya.com', + account_number: '1234567890', + }, + + sales: 'Andi Wijaya', + + product: { + id: 1, + name: 'Live Chicken', + brand: 'LTI Farm', + sku: 'LC-001', + product_price: 18_000, + selling_price: 20_000, + tax: 0, + expiry_period: 0, + uom: { + id: 1, + name: 'Kg', + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + product_category: { + id: 1, + code: 'BROILER', + name: 'Broiler Chicken', + created_user: { + id: 1, + id_user: 101, + email: 'admin@example.com', + name: 'Admin User', + }, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + suppliers: [], + flags: ['LIVE'], + }, + + do_number: 'DO-2025-0001', + vehicle_number: 'B 1234 CD', + marketing_type: 'REGULAR', + + qty: 1000, + average_weight_kg: 1.8, + total_weight_kg: 1800, + + sales_price_per_kg: 20_000, + hpp_price_per_kg: 18_000, + + sales_amount: 36_000_000, + hpp_amount: 32_400_000, + }, + ], + + summary: { + total_qty: 1000, + total_weight_kg: 1800, + total_sales_amount: 36_000_000, + total_hpp_amount: 32_400_000, + }, + }, + }; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts index 16cf24cf..9513760c 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -9,10 +9,26 @@ import { ClosingOutgoingSapronak, ClosingOverhead, ClosingSapronakCalculation, + ClosingProductionData, + ClosingHppExpedition, } from '@/types/api/closing'; import { BaseApiResponse } from '@/types/api/api-general'; import { httpClient, httpClientFetcher } from '@/services/http/client'; import { ClosingSales } from '@/types/api/closing'; + +// TODO: delete these dummy data later +import { + dummyGetAllFetcher, + dummyGetSingle, + dummyGetAllIncomingSapronakFetcher, + dummyGetAllOutgoingSapronakFetcher, + dummyGetGeneralInfo, + dummyGetPerhitunganSapronak, + dummyGetOverhead, + dummyClosingProductionData, +} from '@/dummy/closing.dummy'; +import { sleep } from '@/lib/helper'; + export class ClosingApiService extends BaseApiService { constructor(basePath: string) { super(basePath); @@ -71,6 +87,24 @@ export class ClosingApiService extends BaseApiService { } } + async getProductionData( + id: number + ): Promise | undefined> { + try { + const getProductionDataPath = `${this.basePath}/${id}/production-data`; + const getProductionDataRes = await httpClient< + BaseApiResponse + >(getProductionDataPath); + + return getProductionDataRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + async getPerhitunganSapronak( id: number ): Promise | undefined> { @@ -123,6 +157,24 @@ export class ClosingApiService extends BaseApiService { return undefined; } } + + async getHppEkspedisi( + id: number + ): Promise | undefined> { + try { + const getHppEkspedisiPath = `${this.basePath}/${id}/expedition-hpp`; + const getHppEkspedisiRes = + await httpClient>( + getHppEkspedisiPath + ); + return getHppEkspedisiRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } } export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/services/api/report/logistic-stock.ts b/src/services/api/report/logistic-stock.ts new file mode 100644 index 00000000..13ac5032 --- /dev/null +++ b/src/services/api/report/logistic-stock.ts @@ -0,0 +1,54 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { LogisticPurchasePerSupplierReport } from '@/types/api/report/logistic-stock'; + +export class LogisticApiService extends BaseApiService< + LogisticPurchasePerSupplierReport, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getLogisticPurchasePerSupplierReport( + area_id?: string, + supplier_id?: string, + product_id?: string, + product_category_id?: string, + received_date?: string, + po_date?: string, + start_date?: string, + end_date?: string, + sort_by?: string, + filter_by?: string, + page?: number, + limit?: number + ): Promise | undefined> { + return await this.customRequest< + BaseApiResponse + >(`purchase-supplier`, { + method: 'GET', + params: { + area_id: area_id, + supplier_id: supplier_id, + product_id: product_id, + product_category_id: product_category_id, + received_date: received_date, + po_date: po_date, + start_date: start_date, + end_date: end_date, + sort_by: sort_by, + filter_by: filter_by, + page: page, + limit: limit, + }, + }); + } +} + +export const LogisticApi = new LogisticApiService('reports'); + +// export const LogisticApi = new LogisticApiService( +// 'http://localhost:4010/api/reports/logistics' +// ); diff --git a/src/services/api/report/marketing-report.ts b/src/services/api/report/marketing-report.ts new file mode 100644 index 00000000..b1bcafae --- /dev/null +++ b/src/services/api/report/marketing-report.ts @@ -0,0 +1,75 @@ +import * as XLSX from 'xlsx'; +import toast from 'react-hot-toast'; + +import { BaseApiService } from '@/services/api/base'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { DailyMarketingReport } from '@/types/api/report/marketing'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { formatDate, sleep } from '@/lib/helper'; + +export class MarketingReportApiService extends BaseApiService< + DailyMarketingReport, + unknown, + unknown +> { + constructor(basePath: string = '/reports/marketings/daily-marketing') { + super(basePath); + } + + async getAllDailyMarketingFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async exportDailyMarketingToExcel(initialQueryString: string) { + const params = new URLSearchParams(initialQueryString); + + params.set('limit', '9999999'); + + const queryString = `?${params.toString()}`; + + try { + const dailyMarketingsReport = await httpClientFetcher< + BaseApiResponse + >(`${this.basePath}${queryString}`); + + if (isResponseError(dailyMarketingsReport)) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + return; + } + + const rows = dailyMarketingsReport.data.rows; + + const formattedRows = []; + + for (let i = 0; i < rows.length; i++) { + formattedRows.push({ + ...rows[i], + created_user: rows[i].created_user.name, + created_at: formatDate(rows[i].created_at, 'YYYY-MM-DD'), + updated_at: formatDate(rows[i].updated_at, 'YYYY-MM-DD'), + warehouse: rows[i].warehouse.name, + customer: rows[i].customer.name, + product: rows[i].product.name, + }); + } + + const ws = XLSX.utils.json_to_sheet(formattedRows); + const wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'laporan-penjualan-harian'); + + // triggers download in browser + XLSX.writeFile(wb, 'laporan-penjualan-harian.xlsx'); + } catch (error) { + toast.error('Gagal melakukan export penjualan harian! Coba lagi.'); + } + } +} + +export const MarketingReportApi = new MarketingReportApiService( + '/reports/marketings/daily-marketing' +); 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/closing.d.ts b/src/types/api/closing.d.ts index 04eca605..c23354f8 100644 --- a/src/types/api/closing.d.ts +++ b/src/types/api/closing.d.ts @@ -23,6 +23,33 @@ export type BaseSales = { payment_status: string; }; +export type BaseClosingSales = { + project_type: string; + flock_id: number; + period: number; + sales: BaseSales[]; +}; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Product } from '@type/api/master-data/product'; +import { Customer } from '@type/api/master-data/customer'; +import { BaseMetadata } from '@/types/api/api-general'; + +export type BaseSales = { + id: number; + realization_date: string; + age: number; + do_number: string; + product: Product; + customer: Customer; + qty: number; + weight: number; + avg_weight: number; + price: number; + total_price: number; + kandang: Kandang; + payment_status: string; +}; + export type BaseClosingSales = { project_type: string; flock_id: number; @@ -79,6 +106,44 @@ export type ClosingIncomingSapronak = { export type ClosingOutgoingSapronak = ClosingIncomingSapronak; +export type ClosingProductionData = { + purchase: { + initial_population: number; + claim_culling: number; + final_population: number; + feed_in: number; + feed_used: number; + feed_used_per_head: number; + }; + + sales: { + chicken: { + sales_population: number; + sales_weight: number; + average_weight: number; + chicken_average_selling_price: number; + }; + egg?: { + egg_pieces: number; + egg_mass_kg: number; + average_egg_weight_kg: number; + egg_average_selling_price: number; + }; + }; + + performance: { + depletion: number; + age_day: number; + mortality_std: number; + mortality_act: number; + deff_mortality: number; + fcr_std: number; + fcr_act: number; + deff_fcr: number; + awg: number; + }; +}; + // ====== PERHITUNGAN SAPRONAK ====== export type RowSapronakCalculation = { @@ -141,6 +206,7 @@ export type OverheadTotal = { actual_total_amount: number; cost_per_bird: number; }; + export type ClosingSales = BaseMetadata & BaseClosingSales; // ====== FINANCE ====== @@ -217,3 +283,16 @@ export interface DataSummarySubTotal { rp_per_kg: number; amount: number; } + +export type BaseExpeditionCost = { + id: number; + expedition_vendor_name: string; + hpp_amount: number; +}; + +export type BaseHppExpedition = { + expedition_costs: BaseExpeditionCost[]; + total_hpp_amount: number; +}; + +export type ClosingHppExpedition = BaseMetadata & BaseHppExpedition; diff --git a/src/types/api/master-data/kandang.d.ts b/src/types/api/master-data/kandang.d.ts index c9c14882..eafa0334 100644 --- a/src/types/api/master-data/kandang.d.ts +++ b/src/types/api/master-data/kandang.d.ts @@ -10,7 +10,6 @@ export type BaseKandang = { capacity: number; pic: BaseUser; project_flock_kandang_id?: number; - capacity: number; }; export type Kandang = BaseMetadata & BaseKandang; 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; +}; diff --git a/src/types/api/report/logistic-stock.d.ts b/src/types/api/report/logistic-stock.d.ts new file mode 100644 index 00000000..e5f0f2c6 --- /dev/null +++ b/src/types/api/report/logistic-stock.d.ts @@ -0,0 +1,35 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { Supplier } from '@/types/api/supplier/supplier'; +import { Product } from '@/types/api/product/product'; +import { Warehouse } from '@/types/api/warehouse/warehouse'; + +export type LogisticPurchasePerSupplierReportRow = { + receive_date: string; + po_date: string; + po_number: string; + product: Product; + warehouse: Warehouse; + qty: number; + unit_price: number; + purchase_value: number; + transport_unit_price: number; + transport_value: number; + total_amount: number; + expedition: string; + delivery_number: string; +}; + +export type LogisticPurchasePerSupplierSummary = { + total_qty: number; + total_unit_price: number; + total_purchase_value: number; + total_transport_unit_price: number; + total_transport_value: number; + total_amount: number; +}; + +export type LogisticPurchasePerSupplierReport = BaseMetadata & { + supplier: Supplier; + rows: LogisticPurchasePerSupplierReportRow[]; + summary: LogisticPurchasePerSupplierSummary; +}; diff --git a/src/types/api/report/marketing.d.ts b/src/types/api/report/marketing.d.ts new file mode 100644 index 00000000..d1e81f77 --- /dev/null +++ b/src/types/api/report/marketing.d.ts @@ -0,0 +1,61 @@ +import { BaseMetadata } from '@/types/api/api-general'; +import { BaseCustomer, Customer } from '@/types/api/master-data/customer'; +import { + BaseWarehouseArea, + BaseWarehouseKandang, + BaseWarehouseLocation, + Warehouse, +} from '@/types/api/master-data/warehouse'; +import { Location } from '@/types/api/master-data/location'; +import { Area } from '@/types/api/master-data/area'; +import { BaseProduct } from '@/types/api/master-data/product'; + +export type BaseDailyMarketingRow = { + no: number; + so_date: string; // e.g. "01-Dec-2025" + do_date: string; // e.g. "08-Dec-2025" + aging_days: number; + + warehouse: BaseWarehouseArea | BaseWarehouseLocation | BaseWarehouseKandang; + customer: BaseCustomer; + sales: string; + product: BaseProduct; + + do_number: string; + vehicle_number: string; + marketing_type: string; + + qty: number; + average_weight_kg: number; + total_weight_kg: number; + + sales_price_per_kg: number; + hpp_price_per_kg: number; + + sales_amount: number; + hpp_amount: number; +}; + +export type DailyMarketingRow = BaseMetadata & BaseDailyMarketingRow; + +export interface SalesSummary { + total_qty: number; + total_weight_kg: number; + total_sales_amount: number; + total_hpp_amount: number; +} + +export type DailyMarketingReport = { + rows: DailyMarketingRow[]; + summary: SalesSummary; +}; + +export type MarketingReportFilters = { + area_id?: number; + location_id?: number; + warehouse_id?: number; + customer_id?: number; + start_date?: string; + end_date?: string; + date_type?: 'realized' | 'transaction'; +};