mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
fix(FE): resolve git pull merge development
This commit is contained in:
Generated
+12
-1
@@ -1871,6 +1871,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -1947,6 +1948,7 @@
|
|||||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.2",
|
"@typescript-eslint/scope-manager": "8.46.2",
|
||||||
"@typescript-eslint/types": "8.46.2",
|
"@typescript-eslint/types": "8.46.2",
|
||||||
@@ -2470,6 +2472,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3135,7 +3138,8 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/daisyui": {
|
"node_modules/daisyui": {
|
||||||
"version": "5.5.8",
|
"version": "5.5.8",
|
||||||
@@ -3601,6 +3605,7 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3774,6 +3779,7 @@
|
|||||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.9",
|
"array-includes": "^3.1.9",
|
||||||
@@ -5238,6 +5244,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"fast-png": "^6.2.0",
|
"fast-png": "^6.2.0",
|
||||||
@@ -6338,6 +6345,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -6368,6 +6376,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -7301,6 +7310,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -7468,6 +7478,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ const ClosingDetailPage = () => {
|
|||||||
() => ClosingApi.getPenjualan(Number(closingId))
|
() => ClosingApi.getPenjualan(Number(closingId))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: hppEkspedisiData, isLoading: isLoadingHppEkspedisi } = useSWR(
|
||||||
|
closingId ? `hpp-ekspedisi-${closingId}` : null,
|
||||||
|
() => ClosingApi.getHppEkspedisi(Number(closingId))
|
||||||
|
);
|
||||||
|
|
||||||
if (!closingId) {
|
if (!closingId) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
@@ -39,7 +44,7 @@ const ClosingDetailPage = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = isLoadingClosing || isLoadingSales;
|
const isLoading = isLoadingClosing || isLoadingSales || isLoadingHppEkspedisi;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full p-4 flex flex-row justify-center'>
|
<div className='w-full p-4 flex flex-row justify-center'>
|
||||||
@@ -50,6 +55,11 @@ const ClosingDetailPage = () => {
|
|||||||
id={Number(closingId)}
|
id={Number(closingId)}
|
||||||
initialValue={closing.data}
|
initialValue={closing.data}
|
||||||
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
salesData={isResponseSuccess(salesData) ? salesData.data : undefined}
|
||||||
|
hppExpeditionData={
|
||||||
|
isResponseSuccess(hppEkspedisiData)
|
||||||
|
? hppEkspedisiData.data
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import LogisticStockTabs from '@/components/pages/report/logistic-stock/LogisticStockTabs';
|
||||||
|
|
||||||
|
const LogisticStock = () => {
|
||||||
|
return <LogisticStockTabs />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogisticStock;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) => {
|
||||||
|
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import MarketingReportContent from '@/components/pages/report/MarketingReportContent';
|
||||||
|
|
||||||
|
const MarketingReportPage = () => {
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<MarketingReportContent />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketingReportPage;
|
||||||
@@ -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<HTMLDivElement>(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 (
|
||||||
|
<div className={getWrapperClasses()}>
|
||||||
|
{trigger}
|
||||||
|
{open && !close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={dropdownRef} className={getWrapperClasses()}>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
className={getTriggerClasses()}
|
||||||
|
onClick={toggleDropdown}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleDropdown();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trigger}
|
||||||
|
</div>
|
||||||
|
{!close && (
|
||||||
|
<div tabIndex={-1} className={getContentClasses()}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dropdown;
|
||||||
@@ -5,6 +5,8 @@ import Tooltip from '@/components/Tooltip';
|
|||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
type FloatingActionsButtonProps = {
|
type FloatingActionsButtonProps = {
|
||||||
actions: {
|
actions: {
|
||||||
action: 'DETAIL' | 'EDIT' | 'DELETE';
|
action: 'DETAIL' | 'EDIT' | 'DELETE';
|
||||||
@@ -13,6 +15,7 @@ type FloatingActionsButtonProps = {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
permissions?: string | string[];
|
||||||
}[];
|
}[];
|
||||||
approvals: {
|
approvals: {
|
||||||
action: 'APPROVED' | 'REJECTED';
|
action: 'APPROVED' | 'REJECTED';
|
||||||
@@ -20,6 +23,7 @@ type FloatingActionsButtonProps = {
|
|||||||
label?: string;
|
label?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
permissions?: string | string[];
|
||||||
}[];
|
}[];
|
||||||
selectedRowIds: number[];
|
selectedRowIds: number[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -31,6 +35,7 @@ const FloatingActionsButton = ({
|
|||||||
selectedRowIds,
|
selectedRowIds,
|
||||||
onClose,
|
onClose,
|
||||||
}: FloatingActionsButtonProps) => {
|
}: FloatingActionsButtonProps) => {
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
|
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
|
||||||
const positionStyles =
|
const positionStyles =
|
||||||
selectedRowIds.length > 0
|
selectedRowIds.length > 0
|
||||||
@@ -71,7 +76,18 @@ const FloatingActionsButton = ({
|
|||||||
<div className='flex gap-4 items-center'>
|
<div className='flex gap-4 items-center'>
|
||||||
{/* Render Aksi dari props.actions */}
|
{/* Render Aksi dari props.actions */}
|
||||||
{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) => {
|
.map((action, index) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@@ -111,7 +127,19 @@ const FloatingActionsButton = ({
|
|||||||
|
|
||||||
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
|
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
|
||||||
<div className={`grid grid-cols-${approvals.length} gap-3`}>
|
<div className={`grid grid-cols-${approvals.length} gap-3`}>
|
||||||
{approvals.map((approval, index) => (
|
{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) => (
|
||||||
<Button
|
<Button
|
||||||
key={index}
|
key={index}
|
||||||
onClick={approval.onClick}
|
onClick={approval.onClick}
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import Drawer from '@/components/Drawer';
|
|||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import SidebarMenu from '@/components/molecules/SidebarMenu';
|
import SidebarMenu from '@/components/molecules/SidebarMenu';
|
||||||
|
import PermissionNotFound from '@/components/helper/PermissionNotFound';
|
||||||
|
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
import { MAIN_DRAWER_LINKS } from '@/config/constant';
|
||||||
import { isPathActive } from '@/lib/helper';
|
import { isPathActive } from '@/lib/helper';
|
||||||
|
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -62,6 +65,11 @@ const MainDrawer = ({
|
|||||||
}>) => {
|
}>) => {
|
||||||
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
|
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
|
||||||
|
const isPermitted = ROUTE_PERMISSIONS[pathname]?.some((permission) =>
|
||||||
|
permissionCheck(permission)
|
||||||
|
);
|
||||||
|
|
||||||
const getPageTitle = useCallback(() => {
|
const getPageTitle = useCallback(() => {
|
||||||
let title = '';
|
let title = '';
|
||||||
@@ -101,6 +109,10 @@ const MainDrawer = ({
|
|||||||
setMainDrawerOpen(!mainDrawerOpen);
|
setMainDrawerOpen(!mainDrawerOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isPermitted) {
|
||||||
|
return <PermissionNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
open={mainDrawerOpen}
|
open={mainDrawerOpen}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
|
||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { AuthApi } from '@/services/api/auth';
|
import { AuthApi } from '@/services/api/auth';
|
||||||
@@ -54,8 +54,8 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
|||||||
|
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
direction='bottom'
|
|
||||||
align='end'
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
trigger={
|
trigger={
|
||||||
<div className='btn btn-ghost btn-circle avatar'>
|
<div className='btn btn-ghost btn-circle avatar'>
|
||||||
<div className='w-10 rounded-full border flex justify-center items-center'>
|
<div className='w-10 rounded-full border flex justify-center items-center'>
|
||||||
@@ -67,7 +67,7 @@ const Navbar = ({ title, toggleSidebar }: NavbarProps) => {
|
|||||||
content: 'w-52 mt-3',
|
content: 'w-52 mt-3',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu className='p-2 bg-base-100 shadow rounded-box menu-sm'>
|
<Menu>
|
||||||
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
<MenuItem title='Logout' onClick={logoutClickHandler} />
|
||||||
</Menu>
|
</Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
+11
-4
@@ -21,6 +21,7 @@ export interface TabsProps
|
|||||||
className?:
|
className?:
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
|
container?: string;
|
||||||
wrapper?: string;
|
wrapper?: string;
|
||||||
tab?: string;
|
tab?: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
@@ -53,8 +54,12 @@ const Tabs = ({
|
|||||||
onTabChange?.(tabId);
|
onTabChange?.(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { wrapper: wrapperClassName, tab: tabClassName } =
|
const {
|
||||||
typeof className === 'object'
|
container: containerClassName,
|
||||||
|
wrapper: wrapperClassName,
|
||||||
|
tab: tabClassName,
|
||||||
|
content: contentClassName,
|
||||||
|
} = typeof className === 'object'
|
||||||
? className
|
? className
|
||||||
: { wrapper: className, tab: undefined };
|
: { wrapper: className, tab: undefined };
|
||||||
|
|
||||||
@@ -104,7 +109,7 @@ const Tabs = ({
|
|||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full',
|
'w-full',
|
||||||
typeof className === 'string' ? className : undefined
|
typeof className === 'string' ? className : containerClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div role='tablist' className={getTabsClasses()}>
|
<div role='tablist' className={getTabsClasses()}>
|
||||||
@@ -121,7 +126,9 @@ const Tabs = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeContent && <div className='mt-4'>{activeContent}</div>}
|
{activeContent && (
|
||||||
|
<div className={cn('mt-4', contentClassName)}>{activeContent}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
const PermissionNotFound = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
|
||||||
|
<h2 className='text-2xl font-bold text-error'>Permission Not Found</h2>
|
||||||
|
<p className='text-gray-600 text-center'>
|
||||||
|
You do not have permission to access this page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionNotFound;
|
||||||
@@ -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;
|
||||||
@@ -8,6 +8,7 @@ interface MenuItemProps {
|
|||||||
href?: string;
|
href?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@@ -17,6 +18,7 @@ const MenuItem = ({
|
|||||||
href,
|
href,
|
||||||
icon,
|
icon,
|
||||||
active = false,
|
active = false,
|
||||||
|
isLoading = false,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
}: MenuItemProps) => {
|
}: MenuItemProps) => {
|
||||||
@@ -50,17 +52,28 @@ const MenuItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
{href && (
|
{!isLoading && href && (
|
||||||
<Link href={href} className={menuItemBaseClassName}>
|
<Link href={href} className={menuItemBaseClassName}>
|
||||||
{menuItemContent}
|
{menuItemContent}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!href && (
|
{!isLoading && !href && (
|
||||||
<button className={menuItemBaseClassName} onClick={onClick}>
|
<button className={menuItemBaseClassName} onClick={onClick}>
|
||||||
{menuItemContent}
|
{menuItemContent}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<button className={menuItemBaseClassName}>
|
||||||
|
<span
|
||||||
|
className={cn('loading loading-dots loading-md mx-auto', {
|
||||||
|
'text-gray-400': !active,
|
||||||
|
'text-black': active,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Link from 'next/link';
|
|||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { cn, isPathActive } from '@/lib/helper';
|
import { cn, isPathActive } from '@/lib/helper';
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
export interface SidebarMenuItem {
|
export interface SidebarMenuItem {
|
||||||
type?: 'item' | 'title';
|
type?: 'item' | 'title';
|
||||||
@@ -9,6 +10,7 @@ export interface SidebarMenuItem {
|
|||||||
link: string;
|
link: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
submenu?: SidebarMenuItem[];
|
submenu?: SidebarMenuItem[];
|
||||||
|
permission?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarMenuItemProps {
|
interface SidebarMenuItemProps {
|
||||||
@@ -22,8 +24,17 @@ interface SidebarMenuProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
const isItemActive = isPathActive(activeLink, item.link);
|
const isItemActive = isPathActive(activeLink, item.link);
|
||||||
|
|
||||||
|
const isUserPermitted = item.permission
|
||||||
|
? item.permission?.some((permissionName) => permissionCheck(permissionName))
|
||||||
|
: true;
|
||||||
|
|
||||||
|
if (!isUserPermitted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const menuItemWithoutSubmenu = (
|
const menuItemWithoutSubmenu = (
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
@@ -78,13 +89,15 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
|
|||||||
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
|
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
{menu.map((menuItem, menuIdx) => (
|
{menu.map((menuItem, menuIdx) => {
|
||||||
|
return (
|
||||||
<SidebarMenuItem
|
<SidebarMenuItem
|
||||||
key={menuIdx}
|
key={menuIdx}
|
||||||
item={menuItem}
|
item={menuItem}
|
||||||
activeLink={activeLink}
|
activeLink={activeLink}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,27 +6,32 @@ import { Icon } from '@iconify/react';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Tabs from '@/components/Tabs';
|
import Tabs from '@/components/Tabs';
|
||||||
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable';
|
||||||
|
import ClosingSapronakTabContent from '@/components/pages/closing/ClosingSapronakTabContent';
|
||||||
|
import ClosingProductionDataTabContent from '@/components/pages/closing/ClosingProductionDataTabContent';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ClosingGeneralInformation,
|
ClosingGeneralInformation,
|
||||||
BaseClosingSales,
|
BaseClosingSales,
|
||||||
|
ClosingHppExpedition,
|
||||||
} from '@/types/api/closing';
|
} from '@/types/api/closing';
|
||||||
import ClosingSapronakTabContent from './ClosingSapronakTabContent';
|
|
||||||
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent';
|
||||||
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
import ClosingOverheadTabContent from '@/components/pages/closing/ClosingOverheadTabContent';
|
||||||
import SalesReportTable from './sale/SalesReportTable';
|
|
||||||
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
import ClosingFinanceTabContent from '@/components/pages/closing/ClosingFinanceTabContent';
|
||||||
|
import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable';
|
||||||
|
import HppExpeditionReportTable from './hpp-ekspedisi/HppExpeditionReportTable';
|
||||||
|
|
||||||
interface ClosingDetailProps {
|
interface ClosingDetailProps {
|
||||||
id: number;
|
id: number;
|
||||||
initialValue?: ClosingGeneralInformation;
|
initialValue?: ClosingGeneralInformation;
|
||||||
salesData?: BaseClosingSales;
|
salesData?: BaseClosingSales;
|
||||||
|
hppExpeditionData?: ClosingHppExpedition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
||||||
id,
|
id,
|
||||||
initialValue,
|
initialValue,
|
||||||
salesData,
|
salesData,
|
||||||
|
hppExpeditionData,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
const [activeTab, setActiveTab] = useState<string>('sapronak');
|
||||||
|
|
||||||
@@ -55,12 +60,12 @@ const ClosingDetail: React.FC<ClosingDetailProps> = ({
|
|||||||
{
|
{
|
||||||
id: 'hppEkspedisi',
|
id: 'hppEkspedisi',
|
||||||
label: 'HPP Ekspedisi',
|
label: 'HPP Ekspedisi',
|
||||||
content: 'HPP Ekspedisi',
|
content: <HppExpeditionReportTable initialValues={hppExpeditionData} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'dataProduksi',
|
id: 'dataProduksi',
|
||||||
label: 'Data Produksi',
|
label: 'Data Produksi',
|
||||||
content: 'Data Produksi',
|
content: <ClosingProductionDataTabContent projectFlockId={id} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'keuangan',
|
id: 'keuangan',
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className='w-full flex justify-center py-8'>
|
||||||
|
<span className='loading loading-spinner loading-lg' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!productionData || !isResponseSuccess(productionData)) {
|
||||||
|
return (
|
||||||
|
<div className='w-full text-center py-8 text-gray-500'>
|
||||||
|
Gagal memuat data produksi.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}) => (
|
||||||
|
<div className='flex justify-between items-center py-1'>
|
||||||
|
<span className='text-gray-500 text-sm font-medium w-1/2'>{label}</span>
|
||||||
|
<div className='flex gap-2 w-1/2 justify-end items-center'>
|
||||||
|
<span className={valueClassName}>{value}</span>
|
||||||
|
{unit && <span className={unitClassName}>{unit}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full rounded-xl p-8 shadow-sm'>
|
||||||
|
<h2 className='text-lg font-bold mb-8 text-gray-800'>Data Produksi</h2>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-24 gap-y-12 relative'>
|
||||||
|
{/* Left Column */}
|
||||||
|
<div className='space-y-10'>
|
||||||
|
{/* Purchase Section */}
|
||||||
|
<section>
|
||||||
|
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||||
|
Pembelian
|
||||||
|
</h3>
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<DataRow
|
||||||
|
label='Populasi Awal'
|
||||||
|
value={formatNumber(purchase.initial_population)}
|
||||||
|
unit='Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Claim Culling'
|
||||||
|
value={formatNumber(purchase.claim_culling)}
|
||||||
|
unit='Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Populasi Akhir'
|
||||||
|
value={formatNumber(purchase.final_population)}
|
||||||
|
unit='Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Pakan Masuk'
|
||||||
|
value={formatNumber(purchase.feed_in)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Pakan Terpakai'
|
||||||
|
value={formatNumber(purchase.feed_used)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Pakan Terpakai per Ekor'
|
||||||
|
value={formatNumber(purchase.feed_used_per_head)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Sales Section */}
|
||||||
|
<section>
|
||||||
|
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||||
|
Penjualan
|
||||||
|
</h3>
|
||||||
|
<div className='space-y-4'>
|
||||||
|
{/* Chicken Sales */}
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<DataRow
|
||||||
|
label='Penjualan (Ekor)'
|
||||||
|
value={formatNumber(sales.chicken.sales_population)}
|
||||||
|
unit='Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Penjualan (Kg)'
|
||||||
|
value={formatNumber(sales.chicken.sales_weight)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Bobot Rata-Rata'
|
||||||
|
value={formatNumber(sales.chicken.average_weight)}
|
||||||
|
unit='Kg/Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Harga Jual Rata-Rata'
|
||||||
|
value={formatNumber(
|
||||||
|
sales.chicken.chicken_average_selling_price
|
||||||
|
)}
|
||||||
|
unit='Rupiah'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Egg Sales (if available) */}
|
||||||
|
{sales.egg && (
|
||||||
|
<>
|
||||||
|
<div className='h-px bg-gray-100 my-2' />
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<DataRow
|
||||||
|
label='Telur (Butir)'
|
||||||
|
value={formatNumber(sales.egg.egg_pieces)}
|
||||||
|
unit='Butir'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Telur (Kg)'
|
||||||
|
value={formatNumber(sales.egg.egg_mass_kg)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Berat Telur Rata-Rata'
|
||||||
|
value={formatNumber(sales.egg.average_egg_weight_kg)}
|
||||||
|
unit='Kg'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Harga Jual Telur Rata-Rata'
|
||||||
|
value={formatNumber(sales.egg.egg_average_selling_price)}
|
||||||
|
unit='Rupiah'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider Line (Absolute centered) */}
|
||||||
|
<div className='hidden lg:block absolute left-1/2 top-0 bottom-0 w-px bg-gray-200 -translate-x-1/2' />
|
||||||
|
|
||||||
|
{/* Right Column */}
|
||||||
|
<div className='space-y-10 flex flex-col h-full'>
|
||||||
|
{/* Performance Section */}
|
||||||
|
<section>
|
||||||
|
<h3 className='font-bold text-gray-700 mb-4 text-base'>
|
||||||
|
Performance
|
||||||
|
</h3>
|
||||||
|
<div className='space-y-1'>
|
||||||
|
<DataRow
|
||||||
|
label='Deplesi'
|
||||||
|
value={formatNumber(performance.depletion)}
|
||||||
|
unit='Ekor'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Umur'
|
||||||
|
value={formatNumber(performance.age_day)}
|
||||||
|
unit='Hari'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Mortalitas Std'
|
||||||
|
value={formatNumber(performance.mortality_std)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='Mortalitas Act'
|
||||||
|
value={formatNumber(performance.mortality_act)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='DEFF Mortalitas'
|
||||||
|
value={formatNumber(performance.deff_mortality)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='FCR Std'
|
||||||
|
value={formatNumber(performance.fcr_std)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='FCR Act'
|
||||||
|
value={formatNumber(performance.fcr_act)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='DEFF FCR'
|
||||||
|
value={formatNumber(performance.deff_fcr)}
|
||||||
|
unitClassName='hidden'
|
||||||
|
/>
|
||||||
|
<DataRow
|
||||||
|
label='AWG'
|
||||||
|
value={formatNumber(performance.awg)}
|
||||||
|
unit='Gr/Hari'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingProductionDataTabContent;
|
||||||
@@ -15,6 +15,8 @@ import SelectInput, {
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -43,8 +45,8 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
{/* TODO: apply RBAC */}
|
|
||||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
||||||
|
<RequirePermission permissions='lti.closing.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
href={`/closing/detail/?closingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -54,6 +56,7 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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<BaseExpeditionCost>[] =
|
||||||
|
useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => {
|
||||||
|
return <div>{props.row.index + 1}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
Total HPP Ekspedisi
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(totals.totalHpp)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[totals]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className='w-full'>
|
||||||
|
<div className='p-4'>
|
||||||
|
<h2 className='text-xl font-semibold mb-4'>HPP Ekspedisi</h2>
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full bg-base-100',
|
||||||
|
body: 'p-0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={costOfRevenueExpeditionData}
|
||||||
|
columns={costOfRevenueExpeditionColumns}
|
||||||
|
renderFooter={costOfRevenueExpeditionData.length > 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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HppExpeditionReportTable;
|
||||||
@@ -4,6 +4,7 @@ import toast from 'react-hot-toast';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import DropFileInput from '@/components/input/DropFileInput';
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ const ExpenseRealizationContent = ({
|
|||||||
<div>
|
<div>
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{/* TODO: apply RBAC */}
|
<RequirePermission permissions='lti.expense.update.realization'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -72,6 +73,7 @@ const ExpenseRealizationContent = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
Edit Realisasi
|
Edit Realisasi
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,6 +126,7 @@ const ExpenseRealizationContent = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.document.realization'>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<DropFileInput
|
<DropFileInput
|
||||||
name='documents'
|
name='documents'
|
||||||
@@ -154,6 +157,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { useModal } from '@/components/Modal';
|
|||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { formatCurrency, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatDate } from '@/lib/helper';
|
||||||
@@ -255,6 +256,7 @@ const ExpenseRequestContent = ({
|
|||||||
|
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
{isCurrentApprovalOnManager && (
|
{isCurrentApprovalOnManager && (
|
||||||
|
<RequirePermission permissions='lti.expense.approve.manager'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
@@ -264,9 +266,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
||||||
Approve Manager
|
Approve Manager
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCurrentApprovalOnFinance && (
|
{isCurrentApprovalOnFinance && (
|
||||||
|
<RequirePermission permissions='lti.expense.approve.finance'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -276,9 +280,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
<Icon icon='tdesign:money' width={24} height={24} />
|
||||||
Approve Finance
|
Approve Finance
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCurrentApprovalOnRealization && (
|
{isCurrentApprovalOnRealization && (
|
||||||
|
<RequirePermission permissions='lti.expense.complete.expense'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -292,9 +298,16 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
Selesai
|
Selesai
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRejectButton && (
|
{showRejectButton && (
|
||||||
|
<RequirePermission
|
||||||
|
permissions={[
|
||||||
|
'lti.expense.approve.manager',
|
||||||
|
'lti.expense.approve.finance',
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -304,9 +317,11 @@ const ExpenseRequestContent = ({
|
|||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isExpenseCanBeRealized && (
|
{isExpenseCanBeRealized && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
@@ -320,10 +335,12 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
Realisasi
|
Realisasi
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -333,8 +350,10 @@ const ExpenseRequestContent = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
<Icon icon='mdi:pencil-outline' width={24} height={24} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -349,6 +368,7 @@ const ExpenseRequestContent = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -485,6 +505,7 @@ const ExpenseRequestContent = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.document'>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<DropFileInput
|
<DropFileInput
|
||||||
name='documents'
|
name='documents'
|
||||||
@@ -510,11 +531,16 @@ const ExpenseRequestContent = ({
|
|||||||
isLoading={formik.isSubmitting}
|
isLoading={formik.isSubmitting}
|
||||||
className='w-fit self-end'
|
className='w-fit self-end'
|
||||||
>
|
>
|
||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon
|
||||||
|
icon='ic:round-plus'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
|
|||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Expense } from '@/types/api/expense';
|
import { Expense } from '@/types/api/expense';
|
||||||
import { ExpenseApi } from '@/services/api/expense';
|
import { ExpenseApi } from '@/services/api/expense';
|
||||||
@@ -67,6 +68,7 @@ const RowOptionsMenu = ({
|
|||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
|
||||||
|
<RequirePermission permissions='lti.expense.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
href={`/expense/detail/?expenseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -76,20 +78,28 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='warning'
|
color='warning'
|
||||||
className='justify-start text-sm'
|
className='justify-start text-sm'
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRealizationButton && (
|
{showRealizationButton && (
|
||||||
|
<RequirePermission permissions='lti.expense.create.realization'>
|
||||||
<Button
|
<Button
|
||||||
href={`/expense/realization/?expenseId=${props.row.original.id}`}
|
href={`/expense/realization/?expenseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -103,8 +113,10 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Realisasi
|
Realisasi
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -119,6 +131,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
@@ -559,6 +572,7 @@ const ExpensesTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.expense.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense/add'
|
href='/expense/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -568,9 +582,11 @@ const ExpensesTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
<RequirePermission permissions='lti.expense.approve.manager'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='info'
|
color='info'
|
||||||
@@ -581,7 +597,9 @@ const ExpensesTable = () => {
|
|||||||
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
<Icon icon='lucide-lab:farm' width={24} height={24} />
|
||||||
Approve Manager
|
Approve Manager
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.approve.finance'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -592,7 +610,14 @@ const ExpensesTable = () => {
|
|||||||
<Icon icon='tdesign:money' width={24} height={24} />
|
<Icon icon='tdesign:money' width={24} height={24} />
|
||||||
Approve Finance
|
Approve Finance
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission
|
||||||
|
permissions={[
|
||||||
|
'lti.expense.approve.manager',
|
||||||
|
'lti.expense.approve.finance',
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -610,6 +635,7 @@ const ExpensesTable = () => {
|
|||||||
/>
|
/>
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import DateInput from '@/components/input/DateInput';
|
|||||||
import DropFileInput from '@/components/input/DropFileInput';
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
|
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
|
||||||
import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense';
|
import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateExpenseRealizationPayload,
|
CreateExpenseRealizationPayload,
|
||||||
@@ -290,6 +291,7 @@ const ExpenseRealizationForm = ({
|
|||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.document.realization'>
|
||||||
<DropFileInput
|
<DropFileInput
|
||||||
label='Dokumen Realisasi'
|
label='Dokumen Realisasi'
|
||||||
name='documents'
|
name='documents'
|
||||||
@@ -305,6 +307,7 @@ const ExpenseRealizationForm = ({
|
|||||||
inputWrapper: 'h-12 flex items-center',
|
inputWrapper: 'h-12 flex items-center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{formik.values.existing_documents &&
|
{formik.values.existing_documents &&
|
||||||
formik.values.existing_documents.length > 0 && (
|
formik.values.existing_documents.length > 0 && (
|
||||||
@@ -357,6 +360,7 @@ const ExpenseRealizationForm = ({
|
|||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -371,6 +375,7 @@ const ExpenseRealizationForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import DateInput from '@/components/input/DateInput';
|
|||||||
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
|
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
|
||||||
import DropFileInput from '@/components/input/DropFileInput';
|
import DropFileInput from '@/components/input/DropFileInput';
|
||||||
import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense';
|
import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExpenseRequestFormSchema,
|
ExpenseRequestFormSchema,
|
||||||
@@ -385,6 +386,7 @@ const ExpenseRequestForm = ({
|
|||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.expense.document'>
|
||||||
<DropFileInput
|
<DropFileInput
|
||||||
label='Dokumen Pengajuan'
|
label='Dokumen Pengajuan'
|
||||||
name='documents'
|
name='documents'
|
||||||
@@ -400,6 +402,7 @@ const ExpenseRequestForm = ({
|
|||||||
inputWrapper: 'h-12 flex items-center',
|
inputWrapper: 'h-12 flex items-center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{formik.values.existing_documents &&
|
{formik.values.existing_documents &&
|
||||||
formik.values.existing_documents.length > 0 && (
|
formik.values.existing_documents.length > 0 && (
|
||||||
@@ -461,6 +464,7 @@ const ExpenseRequestForm = ({
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.expense.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -475,8 +479,10 @@ const ExpenseRequestForm = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.expense.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -491,6 +497,7 @@ const ExpenseRequestForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Badge from '@/components/Badge';
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
@@ -175,6 +176,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.inventory.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/inventory/adjustment/add'
|
href='/inventory/adjustment/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -184,6 +186,7 @@ const InventoryAdjustmentTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{/* <DebouncedTextInput
|
{/* <DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import SelectInput from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -28,6 +29,7 @@ const RowOptionsMenu = ({
|
|||||||
props: CellContext<Movement, unknown>;
|
props: CellContext<Movement, unknown>;
|
||||||
}) => (
|
}) => (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.inventory.transfer.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -37,6 +39,7 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -145,6 +148,7 @@ const MovementTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full flex flex-row gap-2'>
|
||||||
|
<RequirePermission permissions='lti.inventory.transfer.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/inventory/movement/add'
|
href='/inventory/movement/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -154,6 +158,7 @@ const MovementTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Table from '@/components/Table';
|
|||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
@@ -31,6 +32,7 @@ const RowOptionsMenu = ({
|
|||||||
props: CellContext<InventoryProduct, unknown>;
|
props: CellContext<InventoryProduct, unknown>;
|
||||||
}) => (
|
}) => (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.inventory.product_stock.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -40,6 +42,7 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
|
|
||||||
const RowsOptionsMenu = ({
|
const RowsOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -50,6 +52,7 @@ const RowsOptionsMenu = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
|
<RequirePermission permissions='lti.marketing.delivery_order.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/marketing/detail?marketingId=${props.row.original.id}`}
|
href={`/marketing/detail?marketingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -59,7 +62,15 @@ const RowsOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{props.row.original.latest_approval.step_number != 1 && (
|
{props.row.original.latest_approval.step_number != 1 && (
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
props.row.original.latest_approval.step_number == 3
|
||||||
|
? 'lti.marketing.delivery_order.update'
|
||||||
|
: 'lti.marketing.delivery_order.create'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
href={
|
href={
|
||||||
props.row.original.latest_approval.step_number == 3
|
props.row.original.latest_approval.step_number == 3
|
||||||
@@ -80,8 +91,10 @@ const RowsOptionsMenu = ({
|
|||||||
<Icon icon='mdi:truck' width={16} height={16} />
|
<Icon icon='mdi:truck' width={16} height={16} />
|
||||||
Deliver
|
Deliver
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{props.row.original.latest_approval.step_number != 3 && (
|
{props.row.original.latest_approval.step_number != 3 && (
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
|
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -91,7 +104,9 @@ const RowsOptionsMenu = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -101,6 +116,7 @@ const RowsOptionsMenu = ({
|
|||||||
<Icon icon='mdi:delete-outline' width={16} height={16} />
|
<Icon icon='mdi:delete-outline' width={16} height={16} />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -116,6 +132,7 @@ const MarketingTable = () => {
|
|||||||
);
|
);
|
||||||
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
|
||||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
const { permissionCheck } = useAuth();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -270,10 +287,14 @@ const MarketingTable = () => {
|
|||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<TableToolbar
|
<TableToolbar
|
||||||
addButton={{
|
addButton={
|
||||||
|
permissionCheck('lti.marketing.sales_order.create')
|
||||||
|
? {
|
||||||
href: '/marketing/add/sales-orders',
|
href: '/marketing/add/sales-orders',
|
||||||
label: 'Tambah Sales Order',
|
label: 'Tambah Sales Order',
|
||||||
}}
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
search={{
|
search={{
|
||||||
value: search,
|
value: search,
|
||||||
onChange: searchChangeHandler,
|
onChange: searchChangeHandler,
|
||||||
@@ -281,6 +302,7 @@ const MarketingTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
onClick={approveClickHandler}
|
onClick={approveClickHandler}
|
||||||
@@ -290,7 +312,9 @@ const MarketingTable = () => {
|
|||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
onClick={rejectClickHandler}
|
onClick={rejectClickHandler}
|
||||||
@@ -300,6 +324,7 @@ const MarketingTable = () => {
|
|||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
<TableRowSizeSelector
|
<TableRowSizeSelector
|
||||||
value={pageSize}
|
value={pageSize}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { useState } from 'react';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const MarketingDetail = ({
|
const MarketingDetail = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -134,6 +135,7 @@ const MarketingDetail = ({
|
|||||||
<div className='flex-row flex gap-3'>
|
<div className='flex-row flex gap-3'>
|
||||||
{initialValues?.latest_approval?.step_number == 1 && (
|
{initialValues?.latest_approval?.step_number == 1 && (
|
||||||
<>
|
<>
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
onClick={approveClickHandler}
|
onClick={approveClickHandler}
|
||||||
@@ -145,6 +147,9 @@ const MarketingDetail = ({
|
|||||||
<Icon icon='mdi:check' width={24} height={24} />
|
<Icon icon='mdi:check' width={24} height={24} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.approve'>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
onClick={rejectClickHandler}
|
onClick={rejectClickHandler}
|
||||||
@@ -156,9 +161,17 @@ const MarketingDetail = ({
|
|||||||
<Icon icon='mdi:close' width={24} height={24} />
|
<Icon icon='mdi:close' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{initialValues?.latest_approval?.step_number != 1 && (
|
{initialValues?.latest_approval?.step_number != 1 && (
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
initialValues?.latest_approval?.step_number == 3
|
||||||
|
? 'lti.marketing.delivery_order.update'
|
||||||
|
: 'lti.marketing.delivery_order.create'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
color='success'
|
color='success'
|
||||||
href={
|
href={
|
||||||
@@ -173,6 +186,7 @@ const MarketingDetail = ({
|
|||||||
: 'Tambah '}
|
: 'Tambah '}
|
||||||
Delivery Order
|
Delivery Order
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -413,6 +427,7 @@ const MarketingDetail = ({
|
|||||||
)}
|
)}
|
||||||
<div className='flex flex-row gap-3'>
|
<div className='flex flex-row gap-3'>
|
||||||
{initialValues?.latest_approval?.step_number != 3 && (
|
{initialValues?.latest_approval?.step_number != 3 && (
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.update'>
|
||||||
<Button
|
<Button
|
||||||
color='warning'
|
color='warning'
|
||||||
type='button'
|
type='button'
|
||||||
@@ -421,11 +436,14 @@ const MarketingDetail = ({
|
|||||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.delete'>
|
||||||
<Button color='error' onClick={deleteClickHandler}>
|
<Button color='error' onClick={deleteClickHandler}>
|
||||||
<Icon icon='mdi:delete' width={24} height={24} />
|
<Icon icon='mdi:delete' width={24} height={24} />
|
||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-v
|
|||||||
import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
|
import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
|
||||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||||
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||||
@@ -689,6 +690,7 @@ const MarketingForm = ({
|
|||||||
{/* Actions button */}
|
{/* Actions button */}
|
||||||
{formType == 'edit' && (
|
{formType == 'edit' && (
|
||||||
<div className='flex flex-row justify-start'>
|
<div className='flex flex-row justify-start'>
|
||||||
|
<RequirePermission permissions='lti.marketing.sales_order.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -698,6 +700,7 @@ const MarketingForm = ({
|
|||||||
<Icon icon='mdi:trash' width={24} height={24} />
|
<Icon icon='mdi:trash' width={24} height={24} />
|
||||||
Hapus
|
Hapus
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
@@ -34,6 +35,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.area.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
|
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -43,7 +45,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.area.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
|
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -53,7 +57,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.area.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -192,6 +199,8 @@ const AreasTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.area.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/area/add'
|
href='/master-data/area/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -201,6 +210,8 @@ const AreasTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AreaFormSchema,
|
AreaFormSchema,
|
||||||
@@ -160,6 +161,7 @@ const AreaForm = ({ type = 'add', initialValues }: AreaFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.area.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -174,8 +176,10 @@ const AreaForm = ({ type = 'add', initialValues }: AreaFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.area.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -190,6 +194,7 @@ const AreaForm = ({ type = 'add', initialValues }: AreaFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
@@ -34,6 +35,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.banks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
|
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -43,7 +45,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.banks.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
|
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -53,7 +57,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.banks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -205,6 +212,7 @@ const BanksTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.banks.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/bank/add'
|
href='/master-data/bank/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -214,6 +222,7 @@ const BanksTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BankFormSchema,
|
BankFormSchema,
|
||||||
@@ -208,6 +209,7 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.banks.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -222,8 +224,10 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.banks.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -238,6 +242,7 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Table from '@/components/Table';
|
|||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
@@ -32,6 +33,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.customer.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
|
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -41,6 +43,8 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.customer.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
|
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -50,6 +54,8 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.customer.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -64,6 +70,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -200,6 +207,7 @@ const CustomersTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.customer.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/customer/add'
|
href='/master-data/customer/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -209,6 +217,7 @@ const CustomersTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { UserApi } from '@/services/api/user';
|
import { UserApi } from '@/services/api/user';
|
||||||
import { TYPE_OPTIONS } from '@/config/constant';
|
import { TYPE_OPTIONS } from '@/config/constant';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface CustomerFormProps {
|
interface CustomerFormProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -319,6 +320,7 @@ const CustomerForm = ({
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{formType !== 'add' && (
|
{formType !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.customer.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -333,8 +335,10 @@ const CustomerForm = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{formType !== 'edit' && (
|
{formType !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.customer.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -349,6 +353,7 @@ const CustomerForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Fcr } from '@/types/api/master-data/fcr';
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
@@ -34,6 +35,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.fcr.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -43,7 +45,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.fcr.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -53,7 +57,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -192,6 +199,7 @@ const FcrsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.fcr.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/fcr/add'
|
href='/master-data/fcr/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -201,6 +209,7 @@ const FcrsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FcrFormSchema,
|
FcrFormSchema,
|
||||||
@@ -296,6 +297,7 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.fcr.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -310,8 +312,10 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.fcr.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -326,6 +330,7 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useModal } from '@/components/Modal';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
@@ -32,6 +33,7 @@ const RowsOptions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -46,6 +48,8 @@ const RowsOptions = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -60,6 +64,8 @@ const RowsOptions = ({
|
|||||||
/>
|
/>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -74,6 +80,7 @@ const RowsOptions = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -196,6 +203,7 @@ const FlockTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/flock/add'
|
href='/master-data/flock/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -205,6 +213,7 @@ const FlockTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Icon } from '@iconify/react';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface FlockCustomProps {
|
interface FlockCustomProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -130,6 +131,7 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{formType !== 'add' && (
|
{formType !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -144,7 +146,9 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{formType !== 'edit' && (
|
{formType !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.flocks.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -159,6 +163,7 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
@@ -39,6 +40,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
|
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -48,7 +50,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
|
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -58,7 +62,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -73,6 +79,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -243,6 +250,8 @@ const KandangsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/kandang/add'
|
href='/master-data/kandang/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -252,6 +261,8 @@ const KandangsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
KandangFormSchema,
|
KandangFormSchema,
|
||||||
@@ -285,6 +286,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -299,8 +301,10 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.kandangs.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -315,6 +319,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
@@ -39,6 +40,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.locations.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
|
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -48,7 +50,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.locations.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
|
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -58,7 +62,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.locations.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -73,6 +79,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -230,6 +237,8 @@ const LocationsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.locations.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/location/add'
|
href='/master-data/location/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -239,6 +248,8 @@ const LocationsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LocationFormSchema,
|
LocationFormSchema,
|
||||||
@@ -229,6 +230,7 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.locations.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -243,8 +245,10 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.locations.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -259,6 +263,7 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
@@ -39,6 +40,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
|
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -48,7 +50,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
|
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -58,7 +62,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -73,6 +79,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -242,6 +249,7 @@ const NonstocksTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/nonstock/add'
|
href='/master-data/nonstock/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -251,6 +259,7 @@ const NonstocksTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NonstockFormSchema,
|
NonstockFormSchema,
|
||||||
@@ -298,6 +299,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -312,8 +314,10 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.nonstocks.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -328,6 +332,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
@@ -34,6 +35,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
|
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -43,6 +45,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
|
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -52,6 +57,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -66,6 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -193,6 +202,7 @@ const ProductCategoryTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/product-category/add'
|
href='/master-data/product-category/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -202,6 +212,7 @@ const ProductCategoryTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProductCategoryFormSchema,
|
ProductCategoryFormSchema,
|
||||||
@@ -183,6 +184,7 @@ const ProductCategoryForm = ({
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -197,8 +199,10 @@ const ProductCategoryForm = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.product_categories.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -213,6 +217,7 @@ const ProductCategoryForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
@@ -38,6 +39,7 @@ const RowOptionsMenu = ({
|
|||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => (
|
}) => (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.products.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
|
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -47,6 +49,8 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.products.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
|
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -56,6 +60,8 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.products.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -70,6 +76,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -273,6 +280,7 @@ const ProductsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.products.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/product/add'
|
href='/master-data/product/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -282,6 +290,7 @@ const ProductsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
name='search'
|
name='search'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import SelectInput, {
|
|||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProductFormSchema,
|
ProductFormSchema,
|
||||||
@@ -413,6 +414,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.products.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -427,7 +429,9 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.products.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -442,6 +446,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Table from '@/components/Table';
|
|||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
@@ -32,6 +33,7 @@ const RowOptions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
|
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -46,6 +48,8 @@ const RowOptions = ({
|
|||||||
/>
|
/>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
|
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -60,6 +64,8 @@ const RowOptions = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -74,6 +80,7 @@ const RowOptions = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -219,6 +226,7 @@ const SuppliersTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/supplier/add'
|
href='/master-data/supplier/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -228,6 +236,7 @@ const SuppliersTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface SupplierCustomProps {
|
interface SupplierCustomProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -406,6 +407,7 @@ const SupplierForm = ({
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{formType !== 'add' && (
|
{formType !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -420,8 +422,10 @@ const SupplierForm = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{formType !== 'edit' && (
|
{formType !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.suppliers.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -436,6 +440,7 @@ const SupplierForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
@@ -34,6 +35,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.uoms.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
|
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -43,7 +45,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.uoms.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
|
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -53,7 +57,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.uoms.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +74,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -192,6 +199,7 @@ const UomsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.uoms.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/uom/add'
|
href='/master-data/uom/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -201,6 +209,7 @@ const UomsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
|
|||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UomFormSchema,
|
UomFormSchema,
|
||||||
@@ -160,6 +161,7 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.uoms.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -174,8 +176,10 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.uoms.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -190,6 +194,7 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
@@ -39,6 +40,7 @@ const RowOptionsMenu = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
|
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -48,7 +50,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
|
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -58,7 +62,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -73,6 +79,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -270,6 +277,7 @@ const WarehousesTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row'>
|
<div className='w-full flex flex-row'>
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/master-data/warehouse/add'
|
href='/master-data/warehouse/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -279,6 +287,7 @@ const WarehousesTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
WarehouseFormSchema,
|
WarehouseFormSchema,
|
||||||
@@ -435,6 +436,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -449,8 +451,10 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{type !== 'edit' && (
|
{type !== 'edit' && (
|
||||||
|
<RequirePermission permissions='lti.master.warehouses.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -465,6 +469,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const RowOptionsMenu = ({
|
const RowOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
props,
|
props,
|
||||||
@@ -46,6 +48,7 @@ const RowOptionsMenu = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
|
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -55,7 +58,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{props.row.original.approval.step_name === 'Aktif' && (
|
{props.row.original.approval.step_name === 'Aktif' && (
|
||||||
|
<RequirePermission permissions='lti.production.chickins.create'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
|
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -65,8 +70,10 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:home-import-outline' width={16} height={16} />
|
<Icon icon='mdi:home-import-outline' width={16} height={16} />
|
||||||
Chickin
|
Chickin
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{props.row.original.approval.step_name === 'Pengajuan' && (
|
{props.row.original.approval.step_name === 'Pengajuan' && (
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
|
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -76,7 +83,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -90,6 +99,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -287,6 +297,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col justify-between items-end gap-2'>
|
<div className='w-full flex flex-col justify-between items-end gap-2'>
|
||||||
<div className='flex flex-col sm:flex-row gap-3 w-full'>
|
<div className='flex flex-col sm:flex-row gap-3 w-full'>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.create'>
|
||||||
<Button
|
<Button
|
||||||
color='primary'
|
color='primary'
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
@@ -295,6 +306,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{/* <Button
|
{/* <Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -630,6 +642,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
setRowSelection({});
|
setRowSelection({});
|
||||||
},
|
},
|
||||||
|
permissions: 'lti.production.project_flocks.detail',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: 'DELETE',
|
action: 'DELETE',
|
||||||
@@ -639,6 +652,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
},
|
},
|
||||||
|
permissions: 'lti.production.project_flocks.delete',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
approvals={[
|
approvals={[
|
||||||
@@ -651,6 +665,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
confirmModal.openModal();
|
confirmModal.openModal();
|
||||||
},
|
},
|
||||||
disabled: !canApprove,
|
disabled: !canApprove,
|
||||||
|
permissions: 'lti.production.project_flocks.approve',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'mdi:times',
|
icon: 'mdi:times',
|
||||||
@@ -660,6 +675,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
setApprovalAction('REJECTED');
|
setApprovalAction('REJECTED');
|
||||||
confirmModal.openModal();
|
confirmModal.openModal();
|
||||||
},
|
},
|
||||||
|
permissions: 'lti.production.project_flocks.approve',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
selectedRowIds={selectedRowIds}
|
selectedRowIds={selectedRowIds}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const ProjectFlockChickinDetail = ({
|
const ProjectFlockChickinDetail = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
@@ -484,6 +485,7 @@ const ProjectFlockChickinDetail = ({
|
|||||||
{kandang.kandang.name}
|
{kandang.kandang.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<RequirePermission permissions='lti.production.chickins.create'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='py-1 text-sm'
|
className='py-1 text-sm'
|
||||||
@@ -499,6 +501,7 @@ const ProjectFlockChickinDetail = ({
|
|||||||
height={11}
|
height={11}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from '@/config/approval-line';
|
} from '@/config/approval-line';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ProjectFlockKandangApi } from '@/services/api/production';
|
import { ProjectFlockKandangApi } from '@/services/api/production';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const ProjectFlockDetail = ({
|
const ProjectFlockDetail = ({
|
||||||
projectFlock,
|
projectFlock,
|
||||||
@@ -110,6 +111,7 @@ const ProjectFlockDetail = ({
|
|||||||
leftIconHref='/production/project-flock'
|
leftIconHref='/production/project-flock'
|
||||||
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
|
subtitle={`Created On ${formatDate(projectFlock.created_at, 'MMM DD, YYYY')}`}
|
||||||
>
|
>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.update'>
|
||||||
<Link
|
<Link
|
||||||
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
|
href={`/production/project-flock/detail/edit?projectFlockId=${projectFlock.id}`}
|
||||||
className='p-0'
|
className='p-0'
|
||||||
@@ -120,6 +122,8 @@ const ProjectFlockDetail = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Link>
|
</Link>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
variant='link'
|
variant='link'
|
||||||
className='p-0 text-error'
|
className='p-0 text-error'
|
||||||
@@ -131,6 +135,7 @@ const ProjectFlockDetail = ({
|
|||||||
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
<Icon icon='mdi:trash-can-outline' width={20} height={20} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
{/* Informasi Umum */}
|
{/* Informasi Umum */}
|
||||||
@@ -418,6 +423,7 @@ const ProjectFlockDetail = ({
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</Card>
|
</Card>
|
||||||
<div className='grid grid-cols-4 gap-3'>
|
<div className='grid grid-cols-4 gap-3'>
|
||||||
|
<RequirePermission permissions='lti.production.chickins.create'>
|
||||||
<Link
|
<Link
|
||||||
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
|
href={`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}&projectFlockId=${projectFlock.id}`}
|
||||||
className='m-0 p-0'
|
className='m-0 p-0'
|
||||||
@@ -434,6 +440,8 @@ const ProjectFlockDetail = ({
|
|||||||
Chickin <Icon icon='mdi:checkbox-marked-outline' />
|
Chickin <Icon icon='mdi:checkbox-marked-outline' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.production.project_flock_kandangs.closing'>
|
||||||
<Link
|
<Link
|
||||||
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
href={`/production/project-flock/closing?projectFlockId=${projectFlock.id}&projectFlockKandangId=${selectedKandang?.project_flock_kandang_id}`}
|
||||||
className='m-0 p-0'
|
className='m-0 p-0'
|
||||||
@@ -450,6 +458,7 @@ const ProjectFlockDetail = ({
|
|||||||
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
|
Close <Icon icon='mdi:checkbox-marked-circle-outline' />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import Card from '@/components/Card';
|
|||||||
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
|
import ProjectFlockKandangTable from '@/components/pages/production/project-flock/form/ProjectFlockKandangTable';
|
||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { useUiStore } from '@/stores/ui/ui.store';
|
import { useUiStore } from '@/stores/ui/ui.store';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
|
||||||
|
|
||||||
interface ProjectFlockFormProps {
|
interface ProjectFlockFormProps {
|
||||||
@@ -734,6 +735,7 @@ const ProjectFlockForm = ({
|
|||||||
)}
|
)}
|
||||||
{formType == 'detail' && (
|
{formType == 'detail' && (
|
||||||
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
|
<div className='w-full flex flex-col sm:flex-row gap-2 py-4'>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -749,6 +751,8 @@ const ProjectFlockForm = ({
|
|||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.production.project_flocks.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -764,6 +768,7 @@ const ProjectFlockForm = ({
|
|||||||
<Icon icon='mdi:times' width={24} height={24} />
|
<Icon icon='mdi:times' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
@@ -1127,6 +1132,13 @@ const ProjectFlockForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div> */}
|
</div> */}
|
||||||
{formType !== 'detail' && (
|
{formType !== 'detail' && (
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
formType == 'add'
|
||||||
|
? 'lti.production.project_flocks.create'
|
||||||
|
: 'lti.production.project_flocks.update'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color='primary'
|
color='primary'
|
||||||
@@ -1137,6 +1149,7 @@ const ProjectFlockForm = ({
|
|||||||
<Icon icon='mdi:plus' width={24} height={24} />
|
<Icon icon='mdi:plus' width={24} height={24} />
|
||||||
{formType == 'add' ? 'Add Flock' : 'Update Flock'}
|
{formType == 'add' ? 'Add Flock' : 'Update Flock'}
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import useSWR from 'swr';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
@@ -59,6 +60,7 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.production.recording.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
|
href={`/production/recording/detail/?recordingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +70,8 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.production.recording.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
|
href={`/production/recording/detail/edit/?recordingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -77,7 +81,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
<Icon icon='mdi:pencil-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
{!isApproved && !isRejected && (
|
{!isApproved && !isRejected && (
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
onClick={approveClickHandler}
|
onClick={approveClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -87,8 +93,10 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:check' width={16} height={16} />
|
<Icon icon='material-symbols:check' width={16} height={16} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{!isApproved && !isRejected && (
|
{!isApproved && !isRejected && (
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
onClick={rejectClickHandler}
|
onClick={rejectClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -98,7 +106,9 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:close' width={16} height={16} />
|
<Icon icon='material-symbols:close' width={16} height={16} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
<RequirePermission permissions='lti.production.recording.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -113,6 +123,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -514,6 +525,7 @@ const RecordingTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.production.recording.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/production/recording/add'
|
href='/production/recording/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -523,9 +535,11 @@ const RecordingTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -538,10 +552,16 @@ const RecordingTable = () => {
|
|||||||
}
|
}
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
<Icon
|
||||||
|
icon='material-symbols:check'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -554,9 +574,14 @@ const RecordingTable = () => {
|
|||||||
}
|
}
|
||||||
className='w-full sm:w-fit'
|
className='w-full sm:w-fit'
|
||||||
>
|
>
|
||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon
|
||||||
|
icon='material-symbols:close'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import useSWR from 'swr';
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Badge from '@/components/Badge';
|
import Badge from '@/components/Badge';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
@@ -1492,6 +1493,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
!isRecordingApproved(initialValues) &&
|
!isRecordingApproved(initialValues) &&
|
||||||
!isRecordingRejected(initialValues) && (
|
!isRecordingRejected(initialValues) && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -1509,7 +1511,9 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.production.recording.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -1527,6 +1531,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2696,6 +2701,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
{/* Left side - Detail & Edit actions */}
|
{/* Left side - Detail & Edit actions */}
|
||||||
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
|
<div className='flex flex-col sm:flex-row justify-start gap-2 w-full sm:w-auto'>
|
||||||
{type === 'detail' && deleteRecordingClickHandler && (
|
{type === 'detail' && deleteRecordingClickHandler && (
|
||||||
|
<RequirePermission permissions='lti.production.recording.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -2710,8 +2716,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{type === 'detail' && initialValues && (
|
{type === 'detail' && initialValues && (
|
||||||
|
<RequirePermission permissions='lti.production.recording.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -2726,6 +2734,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Right side actions */}
|
{/* Right side actions */}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import TextInput from '@/components/input/TextInput';
|
|||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
import { TransferToLaying } from '@/types/api/production/transfer-to-laying';
|
||||||
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
import { TransferToLayingApi } from '@/services/api/production/transfer-to-laying';
|
||||||
@@ -56,12 +57,12 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
const showDeleteButton = showEditButton;
|
const showDeleteButton = showEditButton;
|
||||||
|
|
||||||
// TODO: apply RBAC
|
|
||||||
const showApproveButton = showEditButton;
|
const showApproveButton = showEditButton;
|
||||||
const showRejectButton = showEditButton;
|
const showRejectButton = showEditButton;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
|
href={`/production/transfer-to-laying/detail/?transferToLayingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -71,8 +72,10 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{showEditButton && (
|
{showEditButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
|
||||||
<Button
|
<Button
|
||||||
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
|
href={`/production/transfer-to-laying/detail/edit/?transferToLayingId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -82,10 +85,12 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TODO: apply RBAC */}
|
{/* TODO: apply RBAC */}
|
||||||
{showApproveButton && (
|
{showApproveButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -95,8 +100,10 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{showRejectButton && (
|
{showRejectButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -106,8 +113,10 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
{showDeleteButton && (
|
{showDeleteButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -122,6 +131,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
@@ -502,6 +512,7 @@ const TransferToLayingsTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||||
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/production/transfer-to-laying/add'
|
href='/production/transfer-to-laying/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -511,9 +522,11 @@ const TransferToLayingsTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{selectedRowIds.length > 0 && (
|
{selectedRowIds.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -528,7 +541,9 @@ const TransferToLayingsTable = () => {
|
|||||||
/>
|
/>
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -543,6 +558,7 @@ const TransferToLayingsTable = () => {
|
|||||||
/>
|
/>
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import useSWR from 'swr';
|
|||||||
|
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import SelectInput, {
|
import SelectInput, {
|
||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
@@ -500,7 +501,7 @@ const TransferToLayingForm = ({
|
|||||||
<>
|
<>
|
||||||
{isShowApproveRejectButton && (
|
{isShowApproveRejectButton && (
|
||||||
<div className='w-full flex flex-row justify-end gap-2'>
|
<div className='w-full flex flex-row justify-end gap-2'>
|
||||||
{/* TODO: apply RBAC */}
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='success'
|
color='success'
|
||||||
@@ -514,7 +515,9 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.approve'>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -528,6 +531,7 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -788,6 +792,7 @@ const TransferToLayingForm = ({
|
|||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
<div className='flex flex-row justify-start gap-2'>
|
<div className='flex flex-row justify-start gap-2'>
|
||||||
{isShowDeleteButton && (
|
{isShowDeleteButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.delete'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -802,9 +807,11 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type !== 'edit' && isShowEditButton && (
|
{type !== 'edit' && isShowEditButton && (
|
||||||
|
<RequirePermission permissions='lti.production.transfer_to_laying.update'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='warning'
|
color='warning'
|
||||||
@@ -819,6 +826,7 @@ const TransferToLayingForm = ({
|
|||||||
/>
|
/>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -833,6 +841,13 @@ const TransferToLayingForm = ({
|
|||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
type === 'add'
|
||||||
|
? 'lti.production.transfer_to_laying.create'
|
||||||
|
: 'lti.production.transfer_to_laying.update'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color='primary'
|
color='primary'
|
||||||
@@ -842,6 +857,7 @@ const TransferToLayingForm = ({
|
|||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
|||||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -38,6 +39,7 @@ const RowOptionsMenu = ({
|
|||||||
}: RowOptionsMenuProps) => {
|
}: RowOptionsMenuProps) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
|
<RequirePermission permissions='lti.purchase.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/purchase/detail/?purchaseId=${props.row.original.id}`}
|
href={`/purchase/detail/?purchaseId=${props.row.original.id}`}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -47,6 +49,7 @@ const RowOptionsMenu = ({
|
|||||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
{/*<Button*/}
|
{/*<Button*/}
|
||||||
{/* href={`/purchase/detail/edit/?purchaseId=${props.row.original.id}`}*/}
|
{/* href={`/purchase/detail/edit/?purchaseId=${props.row.original.id}`}*/}
|
||||||
@@ -58,6 +61,7 @@ const RowOptionsMenu = ({
|
|||||||
{/* Edit*/}
|
{/* Edit*/}
|
||||||
{/*</Button>*/}
|
{/*</Button>*/}
|
||||||
|
|
||||||
|
<RequirePermission permissions='lti.purchase.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -72,6 +76,7 @@ const RowOptionsMenu = ({
|
|||||||
/>
|
/>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -227,6 +232,7 @@ const PurchaseTable = () => {
|
|||||||
<div className='flex flex-col gap-2 mb-4'>
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||||
<div className='w-full flex flex-row gap-2'>
|
<div className='w-full flex flex-row gap-2'>
|
||||||
|
<RequirePermission permissions='lti.purchase.create'>
|
||||||
<Button
|
<Button
|
||||||
href='/purchase/add'
|
href='/purchase/add'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -236,6 +242,7 @@ const PurchaseTable = () => {
|
|||||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||||
Tambah
|
Tambah
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
} from '@/types/api/purchase/purchase';
|
} from '@/types/api/purchase/purchase';
|
||||||
import { BaseApproval, BaseGroupedApproval } from '@/types/api/api-general';
|
import { BaseApproval, BaseGroupedApproval } from '@/types/api/api-general';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
interface PurchaseOrderStaffApprovalFormProps {
|
interface PurchaseOrderStaffApprovalFormProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -897,11 +898,15 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
{canUpdatePurchaseItems &&
|
{canUpdatePurchaseItems &&
|
||||||
canShowDeleteAddButtons && (
|
canShowDeleteAddButtons && (
|
||||||
|
<RequirePermission permissions='lti.purchase.delete.item'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
|
className='text-sm w-fit'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
removePurchaseItem(formItemIndex)
|
removePurchaseItem(
|
||||||
|
formItemIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
title='Hapus item'
|
title='Hapus item'
|
||||||
>
|
>
|
||||||
@@ -911,6 +916,7 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
height={16}
|
height={16}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ import { toast } from 'react-hot-toast';
|
|||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
|
||||||
const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type='dropdown'>
|
<RowOptionsMenuWrapper type='dropdown'>
|
||||||
|
<RequirePermission permissions='lti.purchase.update'>
|
||||||
<Button
|
<Button
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -52,6 +54,7 @@ const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -59,6 +62,7 @@ const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
|||||||
const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type='dropdown'>
|
<RowOptionsMenuWrapper type='dropdown'>
|
||||||
|
<RequirePermission permissions='lti.purchase.receive'>
|
||||||
<Button
|
<Button
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
variant='ghost'
|
variant='ghost'
|
||||||
@@ -68,6 +72,7 @@ const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
|||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</RowOptionsMenuWrapper>
|
</RowOptionsMenuWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -496,6 +501,7 @@ const PurchaseOrderDetail = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<RequirePermission permissions='lti.purchase.delete.item'>
|
||||||
<Button
|
<Button
|
||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -504,6 +510,7 @@ const PurchaseOrderDetail = ({
|
|||||||
>
|
>
|
||||||
<Icon icon='mdi:trash-can' width={16} height={16} />
|
<Icon icon='mdi:trash-can' width={16} height={16} />
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -632,6 +639,15 @@ const PurchaseOrderDetail = ({
|
|||||||
|
|
||||||
{showApprovalButton && (
|
{showApprovalButton && (
|
||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
approvalStep === 1
|
||||||
|
? 'lti.purchase.approve.staff'
|
||||||
|
: approvalStep === 2
|
||||||
|
? 'lti.purchase.approve.manager'
|
||||||
|
: 'lti.purchase.receive'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleApprovalClick}
|
onClick={handleApprovalClick}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
@@ -641,7 +657,17 @@ const PurchaseOrderDetail = ({
|
|||||||
<Icon icon='material-symbols:check' width={24} height={24} />
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
||||||
Approve
|
Approve
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
|
||||||
|
<RequirePermission
|
||||||
|
permissions={
|
||||||
|
approvalStep === 1
|
||||||
|
? 'lti.purchase.approve.staff'
|
||||||
|
: approvalStep === 2
|
||||||
|
? 'lti.purchase.approve.manager'
|
||||||
|
: 'lti.purchase.receive'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
variant='outline'
|
variant='outline'
|
||||||
color='error'
|
color='error'
|
||||||
@@ -651,6 +677,7 @@ const PurchaseOrderDetail = ({
|
|||||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<OptionType | null>(null);
|
||||||
|
const {
|
||||||
|
setInputValue: setAreaInputValue,
|
||||||
|
options: areaOptions,
|
||||||
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
} = useSelect<Area>(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<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
} = useSelect<Location>(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<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
setInputValue: setWarehouseInputValue,
|
||||||
|
options: warehouseOptions,
|
||||||
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
|
} = useSelect<Warehouse>(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<OptionType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
setInputValue: setCustomerInputValue,
|
||||||
|
options: customerOptions,
|
||||||
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
} = useSelect<Customer>(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<HTMLInputElement>) => {
|
||||||
|
updateFilter('start_date', e.target.value ? e.target.value : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const endDateChangeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
updateFilter('end_date', e.target.value ? e.target.value : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const [selectedMarketingType, setSelectedMarketingType] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
const marketingTypeChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
setSelectedMarketingType(val as OptionType);
|
||||||
|
updateFilter(
|
||||||
|
'marketing_type',
|
||||||
|
val ? ((val as OptionType).value as string) : ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (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<DailyMarketingReport>
|
||||||
|
>(`${MarketingReportApi.basePath}${queryString}`);
|
||||||
|
|
||||||
|
if (isResponseError(dailyMarketingsReport)) {
|
||||||
|
toast.error('Gagal melakukan export penjualan harian! Coba lagi.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openPdf = async () => {
|
||||||
|
const dailyMarketingReportPdfBlob = await pdf(
|
||||||
|
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
|
||||||
|
).toBlob();
|
||||||
|
|
||||||
|
const dailyMarketingReportPdfUrl = URL.createObjectURL(
|
||||||
|
dailyMarketingReportPdfBlob
|
||||||
|
);
|
||||||
|
window.open(dailyMarketingReportPdfUrl, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdf = async () => {
|
||||||
|
const blob = await pdf(
|
||||||
|
<DailyMarketingReportPDF data={dailyMarketingsReport.data} />
|
||||||
|
).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 (
|
||||||
|
<div className='w-full border border-gray-200 p-4'>
|
||||||
|
<div>
|
||||||
|
<h2 className='text-xl font-bold text-center'>Penjualan Harian</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className='flex flex-col gap-4 mb-6'>
|
||||||
|
<div className='grid grid-cols-12 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
options={areaOptions}
|
||||||
|
isLoading={isLoadingAreaOptions}
|
||||||
|
value={selectedArea}
|
||||||
|
onChange={areaChangeHandler}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
options={locationOptions}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
value={selectedLocation}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Gudang'
|
||||||
|
placeholder='Pilih Gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
isLoading={isLoadingWarehouseOptions}
|
||||||
|
value={selectedWarehouse}
|
||||||
|
onChange={warehouseChangeHandler}
|
||||||
|
onInputChange={setWarehouseInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectInput
|
||||||
|
label='Customer'
|
||||||
|
placeholder='Pilih Customer'
|
||||||
|
options={customerOptions}
|
||||||
|
isLoading={isLoadingCustomerOptions}
|
||||||
|
value={selectedCustomer}
|
||||||
|
onChange={customerChangeHandler}
|
||||||
|
onInputChange={setCustomerInputValue}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='startDate'
|
||||||
|
label='Tanggal Awal'
|
||||||
|
placeholder='Tanggal Realisasi'
|
||||||
|
value={tableFilterState.start_date}
|
||||||
|
onChange={startDateChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DateInput
|
||||||
|
name='endDate'
|
||||||
|
label='Tanggal Akhir'
|
||||||
|
placeholder='Tanggal Realisasi'
|
||||||
|
value={tableFilterState.end_date}
|
||||||
|
onChange={endDateChangeHandler}
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-12 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Tipe Marketing'
|
||||||
|
placeholder='Pilih Tipe Marketing'
|
||||||
|
options={MARKETING_TYPE_OPTIONS}
|
||||||
|
value={selectedMarketingType}
|
||||||
|
onChange={marketingTypeChangeHandler}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className='col-span-12 sm:col-span-6 lg:col-span-8 flex flex-wrap sm:justify-end items-end gap-2'>
|
||||||
|
<Button
|
||||||
|
color='primary'
|
||||||
|
// onClick={handleSearch}
|
||||||
|
className='flex-1 sm:flex-none'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
onClick={handleReset}
|
||||||
|
className='flex-1 sm:flex-none'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
align='end'
|
||||||
|
direction='bottom'
|
||||||
|
trigger={
|
||||||
|
<Button>
|
||||||
|
Export{' '}
|
||||||
|
<Icon
|
||||||
|
icon='heroicons-outline:download'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
title='Export to Excel'
|
||||||
|
icon='icon-park-outline:excel'
|
||||||
|
isLoading={isLoadingExportingToExcel}
|
||||||
|
onClick={exportToExcelHandler}
|
||||||
|
className='text-nowrap'
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
title='Export to PDF'
|
||||||
|
icon='icon-park-outline:file-pdf-one'
|
||||||
|
onClick={exportToPdfHandler}
|
||||||
|
className='text-nowrap'
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DailyMarketingsTable
|
||||||
|
dailyMarketingsReportUrl={dailyMarketingsReportUrl}
|
||||||
|
onSetPage={setPage}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onSetPageSize={setPageSize}
|
||||||
|
searchValue={tableFilterState.search}
|
||||||
|
onSearchChange={searchChangeHandler}
|
||||||
|
onFilterByChange={filterByChangeHandler}
|
||||||
|
onSortByChange={sortByChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyMarketingReportContent;
|
||||||
@@ -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 (
|
||||||
|
<Document>
|
||||||
|
<Page
|
||||||
|
style={DailyMarketingReportPDFStyle.page}
|
||||||
|
orientation='landscape'
|
||||||
|
size='A4'
|
||||||
|
>
|
||||||
|
<View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.companyInfoHeader}>
|
||||||
|
<Image
|
||||||
|
style={DailyMarketingReportPDFStyle.companyLogo}
|
||||||
|
src='/assets/img/lti-logo.png'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.companyInfoHeaderDate}>
|
||||||
|
{formatDate(Date.now(), 'DD MMMM YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.companyName}>
|
||||||
|
PT LUMBUNG TELUR INDONESIA
|
||||||
|
</Text>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.companyAddress}>
|
||||||
|
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||||
|
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={DailyMarketingReportPDFStyle.doubleDivider} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.title}>
|
||||||
|
Laporan Penjualan Harian
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
<View style={DailyMarketingReportPDFStyle.table}>
|
||||||
|
{/* Header */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
DailyMarketingReportPDFStyle.tableRow,
|
||||||
|
DailyMarketingReportPDFStyle.tableHeader,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>No</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Tgl SO
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Tgl DO
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>Aging</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Gudang
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Pelanggan
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>Sales</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Produk
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>No DO</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Plat No
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>Tipe</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>Qty</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Rerata
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>Berat</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Hrg Jual
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
HPP/kg
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Total Jual
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.headerText}>
|
||||||
|
Total HPP
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Rows */}
|
||||||
|
{rows.map((row, index) => (
|
||||||
|
<View style={DailyMarketingReportPDFStyle.tableRow} key={index}>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colNo}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSoDate}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatDate(row.so_date, 'DD/MM/YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colDoDate}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatDate(row.do_date, 'DD/MM/YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colAging}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.aging_days}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colWarehouse}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.warehouse?.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colCustomer}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.customer?.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSales}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.sales}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colProduct}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.product?.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colDoNumber}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.do_number}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colVehicle}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.vehicle_number}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colMarketingType}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{row.marketing_type}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colQty}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatNumber(row.qty)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colAvgWeight}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatNumber(row.average_weight_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colTotalWeight}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatNumber(row.total_weight_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSalesPrice}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatCurrency(row.sales_price_per_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colHppPrice}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatCurrency(row.hpp_price_per_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colSalesAmount}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatCurrency(row.sales_amount)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.colHppAmount}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.cellText}>
|
||||||
|
{formatCurrency(row.hpp_amount)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<View style={DailyMarketingReportPDFStyle.summaryContainer}>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.summaryTable}>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||||
|
Total Qty:
|
||||||
|
</Text>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||||
|
{formatNumber(summary?.total_qty ?? 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||||
|
Total Berat (kg):
|
||||||
|
</Text>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||||
|
{formatNumber(summary?.total_weight_kg ?? 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={DailyMarketingReportPDFStyle.summaryRow}>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||||
|
Total Penjualan:
|
||||||
|
</Text>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||||
|
{formatCurrency(summary?.total_sales_amount ?? 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
DailyMarketingReportPDFStyle.summaryRow,
|
||||||
|
{ borderBottomWidth: 0 },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryLabel}>
|
||||||
|
Total HPP:
|
||||||
|
</Text>
|
||||||
|
<Text style={DailyMarketingReportPDFStyle.summaryValue}>
|
||||||
|
{formatCurrency(summary?.total_hpp_amount ?? 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={DailyMarketingReportPDFStyle.footer} fixed>
|
||||||
|
<Text
|
||||||
|
render={({ pageNumber, totalPages }) =>
|
||||||
|
`${pageNumber} / ${totalPages}`
|
||||||
|
}
|
||||||
|
fixed
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyMarketingReportPDF;
|
||||||
@@ -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<HTMLInputElement>;
|
||||||
|
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<SortingState>([]);
|
||||||
|
|
||||||
|
const dailyMarketingColumns: ColumnDef<DailyMarketingRow>[] = [
|
||||||
|
{
|
||||||
|
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) => (
|
||||||
|
<span className='text-nowrap'>{props.row.original.vehicle_number}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Penjualan Harian</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<div className='flex flex-col gap-2 mb-4'>
|
||||||
|
<div className='w-full flex flex-col sm:flex-row justify-start items-end sm:items-center gap-4'>
|
||||||
|
<DebouncedTextInput
|
||||||
|
name='search'
|
||||||
|
placeholder='Cari Penjualan Harian'
|
||||||
|
value={searchValue}
|
||||||
|
onChange={onSearchChange}
|
||||||
|
className={{ wrapper: 'sm:max-w-3xs' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table<DailyMarketingRow>
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyMarketingsTable;
|
||||||
@@ -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: <DailyMarketingReportContent />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'daily-hpp',
|
||||||
|
label: 'HPP Harian Kandang',
|
||||||
|
content: <HppPerKandangTab />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const MarketingReportContent = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState<string>('daily');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full max-w-7xl pb-16'>
|
||||||
|
<Tabs
|
||||||
|
activeTabId={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
tabs={marketingReportTabs}
|
||||||
|
variant='lifted'
|
||||||
|
className={{
|
||||||
|
content: '-m-px pl-px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarketingReportContent;
|
||||||
@@ -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: <PurchasesPerSupplierTab />,
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// 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 (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<Tabs tabs={tabs} variant='lifted' />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogisticStockTabs;
|
||||||
@@ -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']
|
||||||
|
) => (
|
||||||
|
<Document>
|
||||||
|
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<Text style={pdfStyles.mainTitle}>
|
||||||
|
Laporan > Rekapitulasi Pembelian Per Supplier
|
||||||
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Jenis Tanggal:{' '}
|
||||||
|
{params.filter_by === 'received_date'
|
||||||
|
? 'Tanggal Terima'
|
||||||
|
: 'Tanggal PO'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{getParameterText(params).map((param, index) => (
|
||||||
|
<View key={index} style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>{param}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Supplier Sections */}
|
||||||
|
{supplierReports.map(
|
||||||
|
(
|
||||||
|
supplierReport: LogisticPurchasePerSupplierReport,
|
||||||
|
supplierIndex: number
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={supplierReport.supplier.id}
|
||||||
|
style={[
|
||||||
|
pdfStyles.supplierSection,
|
||||||
|
supplierIndex < supplierReports.length - 1
|
||||||
|
? pdfStyles.supplierSectionBreak
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={pdfStyles.supplierTitle}>
|
||||||
|
{supplierReport.supplier.name}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||||
|
<Text>No</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeader}>
|
||||||
|
<Text>Tanggal Terima</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeader}>
|
||||||
|
<Text>Tanggal PO</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeader}>
|
||||||
|
<Text>Referensi</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeader}>
|
||||||
|
<Text>Produk</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeader}>
|
||||||
|
<Text>Tujuan</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Qty</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Harga Beli</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
|
<Text>Nilai Pembelian</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Biaya Transport</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
|
<Text>Total</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Armada</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellHeaderLast}>
|
||||||
|
<Text>Surat Jalan</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body */}
|
||||||
|
{supplierReport.rows.map(
|
||||||
|
(
|
||||||
|
item: LogisticPurchasePerSupplierReport['rows'][number],
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < supplierReport.rows.length - 1
|
||||||
|
? pdfStyles.tableBorderBottom
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||||
|
<Text>{index + 1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCell}>
|
||||||
|
<Text>
|
||||||
|
{formatDate(item.receive_date, 'DD-MMM-YYYY')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCell}>
|
||||||
|
<Text>{formatDate(item.po_date, 'DD-MMM-YYYY')}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCell}>
|
||||||
|
<Text>{item.po_number || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCell}>
|
||||||
|
<Text>{item.product?.name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCell}>
|
||||||
|
<Text>{item.warehouse?.name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.qty || 0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.unit_price || 0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
|
<Text>{formatCurrency(item.purchase_value || 0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>
|
||||||
|
{formatCurrency(item.transport_unit_price || 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
|
<Text>{formatCurrency(item.total_amount || 0)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<View style={pdfStyles.badge}>
|
||||||
|
<Text>{item.expedition || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.tableCellLast}>
|
||||||
|
<Text>{item.delivery_number || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const generatePurchasesPerSupplierPDF = async (
|
||||||
|
data: LogisticPurchasePerSupplierReport[],
|
||||||
|
params: PurchasesPerSupplierExportParams['params']
|
||||||
|
): Promise<void> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('start_date', val || '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const endDateChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(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<LogisticPurchasePerSupplierReport['rows'][0]>[] => {
|
||||||
|
const tableColumns: ColumnDef<
|
||||||
|
LogisticPurchasePerSupplierReport['rows'][0]
|
||||||
|
>[] = [
|
||||||
|
{
|
||||||
|
id: 'no',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>Total</div>,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summary.total_qty) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'price',
|
||||||
|
header: 'Harga Beli (Rp)',
|
||||||
|
accessorKey: 'unit_price',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.unit_price;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summary.total_unit_price) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'purchase_amount',
|
||||||
|
header: 'Value Harga Beli (Rp)',
|
||||||
|
accessorKey: 'purchase_value',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.purchase_value;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summary.total_purchase_value) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'transport',
|
||||||
|
header: 'Transport (Rp)',
|
||||||
|
accessorKey: 'transport_unit_price',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.transport_unit_price;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summary.total_transport_unit_price) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'value_transport',
|
||||||
|
header: 'Value Transport (Rp)',
|
||||||
|
accessorKey: 'transport_value',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.transport_value;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summary.total_transport_value) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total',
|
||||||
|
header: 'Jumlah (Rp)',
|
||||||
|
accessorKey: 'total_amount',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.total_amount;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summary.total_amount) || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 (
|
||||||
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
<Card
|
||||||
|
subtitle='Laporan > Rekapitulasi Pembelian Per Supplier'
|
||||||
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||||
|
>
|
||||||
|
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||||
|
<Button color='primary' onClick={handleSubmit}>
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
<Button color='warning' onClick={resetFilters}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button color='success' isLoading={isAnyExportLoading}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
align='end'
|
||||||
|
>
|
||||||
|
<Menu className='w-32'>
|
||||||
|
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||||
|
<MenuItem title='PDF' onClick={handleExportPdf} />
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
isMulti
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaOptions.filter((opt) =>
|
||||||
|
(tableFilterState.area_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={areaChangeHandler}
|
||||||
|
isLoading={isLoadingAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Supplier'
|
||||||
|
placeholder='Pilih Supplier'
|
||||||
|
isMulti
|
||||||
|
options={supplierOptions}
|
||||||
|
value={supplierOptions.filter((opt) =>
|
||||||
|
(tableFilterState.supplier_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={supplierChangeHandler}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Produk'
|
||||||
|
placeholder='Pilih Produk'
|
||||||
|
isMulti
|
||||||
|
options={productOptions}
|
||||||
|
value={productOptions.filter((opt) =>
|
||||||
|
(tableFilterState.product_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={productChangeHandler}
|
||||||
|
isLoading={isLoadingProducts}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Kategori Produk'
|
||||||
|
placeholder='Pilih Kategori Produk'
|
||||||
|
isMulti
|
||||||
|
options={productCategoryOptions}
|
||||||
|
value={productCategoryOptions.filter((opt) =>
|
||||||
|
(tableFilterState.product_category_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={productCategoryChangeHandler}
|
||||||
|
isLoading={isLoadingProductCategories}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Filter Berdasarkan'
|
||||||
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
|
options={dataTypeOptions}
|
||||||
|
value={
|
||||||
|
dataTypeOptions?.find(
|
||||||
|
(option) => option.value === tableFilterState.filter_by
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
onChange={dataTypeChangeHandler}
|
||||||
|
isLoading={false}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Urutkan Berdasarkan'
|
||||||
|
placeholder='Urutkan Berdasarkan'
|
||||||
|
options={sortByOptions}
|
||||||
|
value={
|
||||||
|
sortByOptions?.find(
|
||||||
|
(option) => option.value === tableFilterState.sort_by
|
||||||
|
) || null
|
||||||
|
}
|
||||||
|
onChange={sortByHandler}
|
||||||
|
isLoading={false}
|
||||||
|
isClearable={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='md:flex md:flex-row grid grid-cols-1 gap-4'>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Awal'
|
||||||
|
name='start_date'
|
||||||
|
placeholder='Pilih Tanggal Awal'
|
||||||
|
value={tableFilterState.start_date}
|
||||||
|
onChange={startDateChangeHandler}
|
||||||
|
/>
|
||||||
|
<DateInput
|
||||||
|
label='Tanggal Akhir'
|
||||||
|
name='end_date'
|
||||||
|
placeholder='Pilih Tanggal Akhir'
|
||||||
|
value={tableFilterState.end_date}
|
||||||
|
onChange={endDateChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isSubmitted ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Silakan pilih filter dan klik tombol Submit untuk menampilkan data.
|
||||||
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Tidak ada data yang dapat ditampilkan...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
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 (
|
||||||
|
<Card
|
||||||
|
key={supplierReport.supplier.id}
|
||||||
|
title={supplierReport.supplier.name}
|
||||||
|
subtitle={`Total Pembelian: ${formatCurrency(totalPurchase)}`}
|
||||||
|
className={{ wrapper: 'w-full' }}
|
||||||
|
variant='bordered'
|
||||||
|
collapsible={true}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
data={supplierReport.rows}
|
||||||
|
columns={tableColumns}
|
||||||
|
pageSize={10}
|
||||||
|
renderFooter={supplierReport.rows.length > 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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
{meta && data.length > 0 && (
|
||||||
|
<div className='mt-6'>
|
||||||
|
<Pagination
|
||||||
|
currentPage={meta.page}
|
||||||
|
totalItems={meta.total_results}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onRowChange={handleRowChange}
|
||||||
|
onNextPage={handleNextPage}
|
||||||
|
onPrevPage={handlePrevPage}
|
||||||
|
rowOptions={[10, 25, 50, 100]}
|
||||||
|
itemsPerPage={meta.limit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PurchasesPerSupplierTab;
|
||||||
@@ -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: <HppPerKandangTab />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className='w-full p-4'>
|
||||||
|
<Tabs tabs={tabs} variant='lifted' />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaleReportTabs;
|
||||||
@@ -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 (
|
||||||
|
<Document>
|
||||||
|
<Page size='A3' orientation='landscape' style={pdfStyles.page}>
|
||||||
|
{/* Title and Parameters */}
|
||||||
|
<View style={pdfStyles.titleSection}>
|
||||||
|
<Text style={pdfStyles.mainTitle}>
|
||||||
|
Laporan > HPP Harian Kandang
|
||||||
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
{getParameterText(params).map((param, index) => (
|
||||||
|
<View key={index} style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>{param}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Rekapitulasi Section */}
|
||||||
|
<View style={pdfStyles.supplierSection}>
|
||||||
|
<Text style={pdfStyles.supplierTitle}>Rekapitulasi</Text>
|
||||||
|
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Rentang BW</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Sisa Ekor</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Sisa Kg</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Produksi Telur (Butir)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Produksi Telur (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Feed (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>DOC (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nilai Nominal Telur</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>HPP Ayam</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nominal Sisa</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body - Rekapitulasi */}
|
||||||
|
{rekapitulasiByWeightRange.map(
|
||||||
|
(group: HppPerKandangPerWeightRange, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < rekapitulasiByWeightRange.length - 1
|
||||||
|
? pdfStyles.tableBorderBottom
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1.2 }]}>
|
||||||
|
<Text>{group.label}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.remaining_chicken_birds)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{formatNumber(group.remaining_chicken_weight_kg)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatNumber(group.avg_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.egg_production_pieces)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(group.egg_production_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>
|
||||||
|
{group.feed_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<Text>
|
||||||
|
{group.doc_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.average_doc_price_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.egg_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatCurrency(group.hpp_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.egg_hpp_rp_per_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(group.remaining_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Detail Per Kandang Section */}
|
||||||
|
<View style={pdfStyles.supplierSectionBreak}>
|
||||||
|
<Text style={pdfStyles.supplierTitle}>Detail Per Kandang</Text>
|
||||||
|
|
||||||
|
<View style={pdfStyles.table}>
|
||||||
|
{/* Table Header */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.5 }]}>
|
||||||
|
<Text>No</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
|
<Text>Kandang</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
|
<Text>Rentang BW</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>Rata-Rata Bobot (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Sisa Ekor</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Sisa Kg (Ayam)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Produksi Telur (Butir)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>Produksi Telur (Kg)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
|
<Text>Feed (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
|
<Text>DOC (Supplier)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Rata-Rata Harga DOC</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nilai Nominal Telur</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
|
<Text>HPP Ayam</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
|
<Text>HPP Telur (RP/KG)</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
|
<Text>Nominal Sisa</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Table Body - Detail Per Kandang */}
|
||||||
|
{data.rows.map((item: HppPerKandangRow, index: number) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableRow,
|
||||||
|
index < data.rows.length - 1
|
||||||
|
? pdfStyles.tableBorderBottom
|
||||||
|
: {},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.5 }]}>
|
||||||
|
<Text>{index + 1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text>{item.kandang?.name || '-'}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.weight_range.weight_min.toFixed(2)} -{' '}
|
||||||
|
{item.weight_range.weight_max.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatNumber(item.avg_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.remaining_chicken_birds)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.remaining_chicken_weight_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.egg_production_pieces)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatNumber(item.egg_production_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.2 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.feed_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text>
|
||||||
|
{item.doc_suppliers
|
||||||
|
?.map(
|
||||||
|
(s: { alias?: string; name: string }) =>
|
||||||
|
s.alias || s.name
|
||||||
|
)
|
||||||
|
.join(' | ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.average_doc_price_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.egg_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 0.8 }]}>
|
||||||
|
<Text>{formatCurrency(item.hpp_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1 }]}>
|
||||||
|
<Text>{formatCurrency(item.egg_hpp_rp_per_kg)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
|
<Text>{formatCurrency(item.remaining_value_rp)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
</Document>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateHppPerKandangPDF = async (
|
||||||
|
data: HppPerKandangExportParams['data'],
|
||||||
|
params: HppPerKandangExportParams['params']
|
||||||
|
): Promise<void> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('weight_min', val ? String(parseFloat(val) || 0) : '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const weightMaxChangeHandler = useCallback<
|
||||||
|
ChangeEventHandler<HTMLInputElement>
|
||||||
|
>(
|
||||||
|
(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
updateFilter('weight_max', val ? String(parseFloat(val) || 0) : '');
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
[updateFilter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const periodChangeHandler = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(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<HppPerKandangReport | null> => {
|
||||||
|
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<string>();
|
||||||
|
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<string>();
|
||||||
|
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<HppPerKandangReport['rows'][0]>[] => {
|
||||||
|
const tableColumns: ColumnDef<HppPerKandangReport['rows'][0]>[] = [
|
||||||
|
{
|
||||||
|
id: 'no',
|
||||||
|
header: 'No',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>TOTAL</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'kandang_name',
|
||||||
|
header: 'Kandang',
|
||||||
|
accessorKey: 'kandang.name',
|
||||||
|
cell: (props) => {
|
||||||
|
const kandang = props.row.original.kandang;
|
||||||
|
return kandang?.name || '-';
|
||||||
|
},
|
||||||
|
footer: () => <div className='font-semibold text-gray-900'>ALL</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => <div className='font-semibold text-gray-900'>-</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'avg_weight_kg',
|
||||||
|
header: 'Rata-Rata Bobot (KG)',
|
||||||
|
accessorKey: 'avg_weight_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.avg_weight_kg;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.average_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remaining_chicken_birds',
|
||||||
|
header: 'Sisa Ayam (Ekor)',
|
||||||
|
accessorKey: 'remaining_chicken_birds',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.remaining_chicken_birds;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_birds || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production_pieces',
|
||||||
|
header: 'Produksi Telur (Butir)',
|
||||||
|
accessorKey: 'egg_production_pieces',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_production_pieces;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_egg_production_pieces || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production_kg',
|
||||||
|
header: 'Produksi Telur (KG)',
|
||||||
|
accessorKey: 'egg_production_kg',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_production_kg;
|
||||||
|
return <div className='text-right'>{formatNumber(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatNumber(summaryTotal?.total_remaining_chicken_weight_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{allFeedSuppliers || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: () => (
|
||||||
|
<div className='font-semibold text-gray-900'>
|
||||||
|
{allDocSuppliers || '-'}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_average_doc_price_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_value_rp',
|
||||||
|
header: 'Nilai Nominal Telur (RP)',
|
||||||
|
accessorKey: 'egg_value_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_value_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_egg_value_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hpp_rp',
|
||||||
|
header: 'HPP Ayam (RP)',
|
||||||
|
accessorKey: 'hpp_rp',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hpp_rp;
|
||||||
|
return <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_hpp_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.average_egg_hpp_rp_per_kg || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <div className='text-right'>{formatCurrency(value)}</div>;
|
||||||
|
},
|
||||||
|
footer: () => (
|
||||||
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
|
{formatCurrency(summaryTotal?.total_remaining_value_rp || 0)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return tableColumns;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== CUSTOM ROW RENDERER =====
|
||||||
|
const renderCustomRow = useCallback(
|
||||||
|
(row: Row<HppPerKandangReport['rows'][0]>) => {
|
||||||
|
if (row.index === data.length - 1) {
|
||||||
|
const defaultRow = (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<td
|
||||||
|
key={cell.id}
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap border-gray-200'
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const customRows = [
|
||||||
|
<tr
|
||||||
|
className='border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
key={'rekapitulasi-row'}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
colSpan={15}
|
||||||
|
className='px-4 py-3 text-gray-900 text-center font-semibold'
|
||||||
|
>
|
||||||
|
Rekapitulasi per rentang bobot
|
||||||
|
</td>
|
||||||
|
</tr>,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (perWeightRangeSummary.length > 0) {
|
||||||
|
perWeightRangeSummary.forEach(
|
||||||
|
(item: HppPerKandangPerWeightRange, index = 0) => {
|
||||||
|
customRows.push(
|
||||||
|
<tr
|
||||||
|
key={`summary-${item.id}`}
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200 [&_td]:px-4 [&_td]:py-3 [&_td]:text-xs [&_td]:text-gray-900 [&_td]:whitespace-nowrap'
|
||||||
|
>
|
||||||
|
<td className=''>{index + 1}</td>
|
||||||
|
<td className=''>ALL</td>
|
||||||
|
<td className=''>{item.label}</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.avg_weight_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.remaining_chicken_birds)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.remaining_chicken_weight_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.egg_production_pieces)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatNumber(item.egg_production_kg)}
|
||||||
|
</td>
|
||||||
|
<td className=''>
|
||||||
|
{item.feed_suppliers
|
||||||
|
?.map((s) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</td>
|
||||||
|
<td className=''>
|
||||||
|
{item.doc_suppliers
|
||||||
|
?.map((s) => s.alias || s.name)
|
||||||
|
.join(' | ') || '-'}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.average_doc_price_rp)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.egg_value_rp)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>{formatCurrency(item.hpp_rp)}</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.egg_hpp_rp_per_kg)}
|
||||||
|
</td>
|
||||||
|
<td className='text-right'>
|
||||||
|
{formatCurrency(item.remaining_value_rp)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [defaultRow, ...customRows];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[data, perWeightRangeSummary]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full p-0 sm:p-4'>
|
||||||
|
<Card
|
||||||
|
subtitle={
|
||||||
|
period
|
||||||
|
? `Laporan > HPP Harian Kandang (${period})`
|
||||||
|
: 'Laporan > HPP Harian Kandang'
|
||||||
|
}
|
||||||
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||||
|
>
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Area'
|
||||||
|
placeholder='Pilih Area'
|
||||||
|
isMulti
|
||||||
|
options={areaOptions}
|
||||||
|
value={areaOptions.filter((opt) =>
|
||||||
|
(tableFilterState.area_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={areaChangeHandler}
|
||||||
|
isLoading={isLoadingAreas}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Lokasi'
|
||||||
|
placeholder='Pilih Lokasi'
|
||||||
|
isMulti
|
||||||
|
options={locationOptions}
|
||||||
|
value={locationOptions.filter((opt) =>
|
||||||
|
(tableFilterState.location_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={locationChangeHandler}
|
||||||
|
isLoading={isLoadingLocations}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Kandang'
|
||||||
|
placeholder='Pilih Kandang'
|
||||||
|
isMulti
|
||||||
|
options={kandangOptions}
|
||||||
|
value={kandangOptions.filter((opt) =>
|
||||||
|
(tableFilterState.kandang_id || [])
|
||||||
|
.map(String)
|
||||||
|
.includes(String(opt.value))
|
||||||
|
)}
|
||||||
|
onChange={kandangChangeHandler}
|
||||||
|
isLoading={isLoadingKandangs}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid md:grid-cols-3 grid-cols-1 gap-4'>
|
||||||
|
<div className='flex flex-row gap-4'>
|
||||||
|
<NumberInput
|
||||||
|
label='Rentang Bobot Min (Kg)'
|
||||||
|
name='weight_min'
|
||||||
|
placeholder='Masukkan bobot minimum'
|
||||||
|
value={tableFilterState.weight_min}
|
||||||
|
onChange={weightMinChangeHandler}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label='Rentang Bobot Max (Kg)'
|
||||||
|
name='weight_max'
|
||||||
|
placeholder='Masukkan bobot maximum'
|
||||||
|
value={tableFilterState.weight_max}
|
||||||
|
onChange={weightMaxChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DateInput
|
||||||
|
label='Periode'
|
||||||
|
name='period'
|
||||||
|
placeholder='Pilih Periode'
|
||||||
|
value={tableFilterState.period}
|
||||||
|
onChange={periodChangeHandler}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<SelectInput
|
||||||
|
label='Tampilkan Kandang Tanpa Recording'
|
||||||
|
placeholder='Pilih Opsi'
|
||||||
|
options={showUnrecordedOptions}
|
||||||
|
value={
|
||||||
|
tableFilterState.show_unrecorded
|
||||||
|
? showUnrecordedOptions.find((opt) => opt.value === 'true') ||
|
||||||
|
null
|
||||||
|
: showUnrecordedOptions.find((opt) => opt.value === 'false') ||
|
||||||
|
null
|
||||||
|
}
|
||||||
|
onChange={showUnrecordedChangeHandler}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||||
|
<Button color='primary' onClick={handleSubmit}>
|
||||||
|
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||||
|
Cari
|
||||||
|
</Button>
|
||||||
|
<Button color='warning' onClick={resetFilters}>
|
||||||
|
<Icon icon='heroicons-outline:refresh' width={20} height={20} />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button color='success' isLoading={isAnyExportLoading}>
|
||||||
|
Export
|
||||||
|
<Icon
|
||||||
|
icon='heroicons-outline:download'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
align='end'
|
||||||
|
>
|
||||||
|
<Menu className='w-32'>
|
||||||
|
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||||
|
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||||
|
</Menu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='divider'></div>
|
||||||
|
|
||||||
|
{!isSubmitted ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Silakan pilih filter dan klik tombol Cari untuk menampilkan data.
|
||||||
|
</div>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||||
|
<span className='loading loading-spinner loading-xl' />
|
||||||
|
</div>
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<div className='mt-6 text-center text-gray-500'>
|
||||||
|
Tidak ada data yang dapat ditampilkan...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table
|
||||||
|
data={data}
|
||||||
|
columns={getTableColumns()}
|
||||||
|
renderFooter={data.length > 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',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HppPerKandangTab;
|
||||||
+76
-1
@@ -10,14 +10,20 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Produksi',
|
text: 'Produksi',
|
||||||
link: '/production',
|
link: '/production',
|
||||||
icon: 'heroicons-outline:wrench-screwdriver',
|
icon: 'heroicons-outline:wrench-screwdriver',
|
||||||
|
permission: [
|
||||||
|
'lti.production.project_flocks.list',
|
||||||
|
'lti.production.recording.list',
|
||||||
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
text: 'Daftar Flock',
|
text: 'Daftar Flock',
|
||||||
link: '/production/project-flock',
|
link: '/production/project-flock',
|
||||||
|
permission: ['lti.production.project_flocks.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Recording',
|
text: 'Recording',
|
||||||
link: '/production/recording',
|
link: '/production/recording',
|
||||||
|
permission: ['lti.production.recording.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Transfer to Laying',
|
text: 'Transfer to Laying',
|
||||||
@@ -29,6 +35,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Pembelian',
|
text: 'Pembelian',
|
||||||
link: '/purchase',
|
link: '/purchase',
|
||||||
icon: 'heroicons-outline:shopping-cart',
|
icon: 'heroicons-outline:shopping-cart',
|
||||||
|
permission: ['lti.purchase.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penjualan',
|
text: 'Penjualan',
|
||||||
@@ -41,14 +48,16 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
icon: 'heroicons-outline:banknotes',
|
icon: 'heroicons-outline:banknotes',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Biaya Operasional',
|
text: 'Biaya',
|
||||||
link: '/expense',
|
link: '/expense',
|
||||||
icon: 'heroicons:wallet',
|
icon: 'heroicons:wallet',
|
||||||
|
permission: ['lti.expense.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Closing',
|
text: 'Closing',
|
||||||
link: '/closing',
|
link: '/closing',
|
||||||
icon: 'heroicons-outline:presentation-chart-bar',
|
icon: 'heroicons-outline:presentation-chart-bar',
|
||||||
|
permission: ['lti.closing.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Laporan',
|
text: 'Laporan',
|
||||||
@@ -63,24 +72,36 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Biaya Operasional',
|
text: 'Biaya Operasional',
|
||||||
link: '/report/expense',
|
link: '/report/expense',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Penjualan',
|
||||||
|
link: '/report/marketing',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Persediaan',
|
text: 'Persediaan',
|
||||||
link: '/inventory',
|
link: '/inventory',
|
||||||
icon: 'heroicons-outline:folder',
|
icon: 'heroicons-outline:folder',
|
||||||
|
permission: [
|
||||||
|
'lti.inventory.product_stock.list',
|
||||||
|
'lti.inventory.product_warehouses.list',
|
||||||
|
'lti.inventory.transfer.list',
|
||||||
|
],
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
text: 'Stok Produk',
|
text: 'Stok Produk',
|
||||||
link: '/inventory/product',
|
link: '/inventory/product',
|
||||||
|
permission: ['lti.inventory.product_stock.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Penyesuaian Stok',
|
text: 'Penyesuaian Stok',
|
||||||
link: '/inventory/adjustment',
|
link: '/inventory/adjustment',
|
||||||
|
permission: ['lti.inventory.product_stock.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Transfer Stok',
|
text: 'Transfer Stok',
|
||||||
link: '/inventory/movement',
|
link: '/inventory/movement',
|
||||||
|
permission: ['lti.inventory.transfer.list'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -88,58 +109,86 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [
|
|||||||
text: 'Master Data',
|
text: 'Master Data',
|
||||||
link: '/master-data',
|
link: '/master-data',
|
||||||
icon: 'heroicons-outline:circle-stack',
|
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: [
|
submenu: [
|
||||||
{
|
{
|
||||||
text: 'Produk',
|
text: 'Produk',
|
||||||
link: '/master-data/product',
|
link: '/master-data/product',
|
||||||
|
permission: ['lti.master.products.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Kategori Produk',
|
text: 'Kategori Produk',
|
||||||
link: '/master-data/product-category',
|
link: '/master-data/product-category',
|
||||||
|
permission: ['lti.master.product_categories.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Bank',
|
text: 'Bank',
|
||||||
link: '/master-data/bank',
|
link: '/master-data/bank',
|
||||||
|
permission: ['lti.master.banks.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Area',
|
text: 'Area',
|
||||||
link: '/master-data/area',
|
link: '/master-data/area',
|
||||||
|
permission: ['lti.master.area.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Lokasi',
|
text: 'Lokasi',
|
||||||
link: '/master-data/location',
|
link: '/master-data/location',
|
||||||
|
permission: ['lti.master.locations.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Kandang',
|
text: 'Kandang',
|
||||||
link: '/master-data/kandang',
|
link: '/master-data/kandang',
|
||||||
|
permission: ['lti.master.kandangs.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Warehouse',
|
text: 'Warehouse',
|
||||||
link: '/master-data/warehouse',
|
link: '/master-data/warehouse',
|
||||||
|
permission: ['lti.master.warehouses.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Customer',
|
text: 'Customer',
|
||||||
link: '/master-data/customer',
|
link: '/master-data/customer',
|
||||||
|
permission: ['lti.master.customer.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'UOM',
|
text: 'UOM',
|
||||||
link: '/master-data/uom',
|
link: '/master-data/uom',
|
||||||
|
permission: ['lti.master.uoms.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Non-Stock',
|
text: 'Non-Stock',
|
||||||
link: '/master-data/nonstock',
|
link: '/master-data/nonstock',
|
||||||
|
permission: ['lti.master.nonstocks.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'FCR',
|
text: 'FCR',
|
||||||
link: '/master-data/fcr',
|
link: '/master-data/fcr',
|
||||||
|
permission: ['lti.master.fcr.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Supplier',
|
text: 'Supplier',
|
||||||
link: '/master-data/supplier',
|
link: '/master-data/supplier',
|
||||||
|
permission: ['lti.master.suppliers.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Flock',
|
text: 'Flock',
|
||||||
link: '/master-data/flock',
|
link: '/master-data/flock',
|
||||||
|
permission: ['lti.master.flocks.list'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Standar Produksi',
|
text: 'Standar Produksi',
|
||||||
@@ -288,3 +337,29 @@ export const ACCEPTED_FILE_TYPE = {
|
|||||||
'image/*': [],
|
'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',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||||
|
'/': ['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'],
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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<DailyMarketingReport> =
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,10 +9,26 @@ import {
|
|||||||
ClosingOutgoingSapronak,
|
ClosingOutgoingSapronak,
|
||||||
ClosingOverhead,
|
ClosingOverhead,
|
||||||
ClosingSapronakCalculation,
|
ClosingSapronakCalculation,
|
||||||
|
ClosingProductionData,
|
||||||
|
ClosingHppExpedition,
|
||||||
} from '@/types/api/closing';
|
} from '@/types/api/closing';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
import { httpClient, httpClientFetcher } from '@/services/http/client';
|
||||||
import { ClosingSales } from '@/types/api/closing';
|
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<Closing, null, null> {
|
export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
||||||
constructor(basePath: string) {
|
constructor(basePath: string) {
|
||||||
super(basePath);
|
super(basePath);
|
||||||
@@ -71,6 +87,24 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProductionData(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseApiResponse<ClosingProductionData> | undefined> {
|
||||||
|
try {
|
||||||
|
const getProductionDataPath = `${this.basePath}/${id}/production-data`;
|
||||||
|
const getProductionDataRes = await httpClient<
|
||||||
|
BaseApiResponse<ClosingProductionData>
|
||||||
|
>(getProductionDataPath);
|
||||||
|
|
||||||
|
return getProductionDataRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<ClosingProductionData>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getPerhitunganSapronak(
|
async getPerhitunganSapronak(
|
||||||
id: number
|
id: number
|
||||||
): Promise<BaseApiResponse<ClosingSapronakCalculation> | undefined> {
|
): Promise<BaseApiResponse<ClosingSapronakCalculation> | undefined> {
|
||||||
@@ -123,6 +157,24 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getHppEkspedisi(
|
||||||
|
id: number
|
||||||
|
): Promise<BaseApiResponse<ClosingHppExpedition> | undefined> {
|
||||||
|
try {
|
||||||
|
const getHppEkspedisiPath = `${this.basePath}/${id}/expedition-hpp`;
|
||||||
|
const getHppEkspedisiRes =
|
||||||
|
await httpClient<BaseApiResponse<ClosingHppExpedition>>(
|
||||||
|
getHppEkspedisiPath
|
||||||
|
);
|
||||||
|
return getHppEkspedisiRes;
|
||||||
|
} catch (error) {
|
||||||
|
if (axios.isAxiosError<BaseApiResponse<ClosingHppExpedition>>(error)) {
|
||||||
|
return error.response?.data;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClosingApi = new ClosingApiService('/closings');
|
export const ClosingApi = new ClosingApiService('/closings');
|
||||||
|
|||||||
@@ -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<BaseApiResponse<LogisticPurchasePerSupplierReport> | undefined> {
|
||||||
|
return await this.customRequest<
|
||||||
|
BaseApiResponse<LogisticPurchasePerSupplierReport>
|
||||||
|
>(`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'
|
||||||
|
// );
|
||||||
@@ -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<BaseApiResponse<DailyMarketingReport>> {
|
||||||
|
return await httpClientFetcher<BaseApiResponse<DailyMarketingReport>>(
|
||||||
|
endpoint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportDailyMarketingToExcel(initialQueryString: string) {
|
||||||
|
const params = new URLSearchParams(initialQueryString);
|
||||||
|
|
||||||
|
params.set('limit', '9999999');
|
||||||
|
|
||||||
|
const queryString = `?${params.toString()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dailyMarketingsReport = await httpClientFetcher<
|
||||||
|
BaseApiResponse<DailyMarketingReport>
|
||||||
|
>(`${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'
|
||||||
|
);
|
||||||
@@ -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<BaseApiResponse<HppPerKandangReport> | undefined> {
|
||||||
|
return await this.customRequest<BaseApiResponse<HppPerKandangReport>>(
|
||||||
|
`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'
|
||||||
|
// );
|
||||||
Vendored
+79
@@ -23,6 +23,33 @@ export type BaseSales = {
|
|||||||
payment_status: string;
|
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 = {
|
export type BaseClosingSales = {
|
||||||
project_type: string;
|
project_type: string;
|
||||||
flock_id: number;
|
flock_id: number;
|
||||||
@@ -79,6 +106,44 @@ export type ClosingIncomingSapronak = {
|
|||||||
|
|
||||||
export type ClosingOutgoingSapronak = 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 ======
|
// ====== PERHITUNGAN SAPRONAK ======
|
||||||
|
|
||||||
export type RowSapronakCalculation = {
|
export type RowSapronakCalculation = {
|
||||||
@@ -141,6 +206,7 @@ export type OverheadTotal = {
|
|||||||
actual_total_amount: number;
|
actual_total_amount: number;
|
||||||
cost_per_bird: number;
|
cost_per_bird: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClosingSales = BaseMetadata & BaseClosingSales;
|
export type ClosingSales = BaseMetadata & BaseClosingSales;
|
||||||
|
|
||||||
// ====== FINANCE ======
|
// ====== FINANCE ======
|
||||||
@@ -217,3 +283,16 @@ export interface DataSummarySubTotal {
|
|||||||
rp_per_kg: number;
|
rp_per_kg: number;
|
||||||
amount: 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;
|
||||||
|
|||||||
-1
@@ -10,7 +10,6 @@ export type BaseKandang = {
|
|||||||
capacity: number;
|
capacity: number;
|
||||||
pic: BaseUser;
|
pic: BaseUser;
|
||||||
project_flock_kandang_id?: number;
|
project_flock_kandang_id?: number;
|
||||||
capacity: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Kandang = BaseMetadata & BaseKandang;
|
export type Kandang = BaseMetadata & BaseKandang;
|
||||||
|
|||||||
+69
@@ -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;
|
||||||
|
};
|
||||||
+35
@@ -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;
|
||||||
|
};
|
||||||
Vendored
+61
@@ -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';
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user