diff --git a/.gitignore b/.gitignore index d86875dd..e47b8ec3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ next-env.d.ts # idea .idea + +# claude +.claude diff --git a/package-lock.json b/package-lock.json index ec1316ae..f0212474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -36,9 +36,9 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.1.12", + "daisyui": "^5.5.8", "eslint": "^9", - "eslint-config-next": "15.5.3", + "eslint-config-next": "^15.5.7", "husky": "^9.1.7", "prettier": "^3.6.2", "tailwindcss": "^4", @@ -1082,15 +1082,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.3.tgz", - "integrity": "sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.3.tgz", - "integrity": "sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.7.tgz", + "integrity": "sha512-DtRU2N7BkGr8r+pExfuWHwMEPX5SD57FeA6pxdgCHODo+b/UgIgjE+rgWKtJAbEbGhVZ2jtHn4g3wNhWFoNBQQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1098,9 +1098,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.3.tgz", - "integrity": "sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1114,9 +1114,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.3.tgz", - "integrity": "sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.3.tgz", - "integrity": "sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -1146,9 +1146,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.3.tgz", - "integrity": "sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -1162,9 +1162,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.3.tgz", - "integrity": "sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -1178,9 +1178,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.3.tgz", - "integrity": "sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -1194,9 +1194,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.3.tgz", - "integrity": "sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -1210,9 +1210,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.3.tgz", - "integrity": "sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -3063,9 +3063,9 @@ "license": "MIT" }, "node_modules/daisyui": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.3.10.tgz", - "integrity": "sha512-vmjyPmm0hvFhA95KB6uiGmWakziB2pBv6CUcs5Ka/3iMBMn9S+C3SZYx9G9l2JrgTZ1EFn61F/HrPcwaUm2kLQ==", + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz", + "integrity": "sha512-6psL9jIEOFOw68V10j/BKCWcRgx8dh81mmNxShr+g7HDM6UHNoPharlp9zq/PQkHNuGU1ZQsajR3HgpvavbRKQ==", "dev": true, "license": "MIT", "funding": { @@ -3571,13 +3571,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.3.tgz", - "integrity": "sha512-e6j+QhQFOr5pfsc8VJbuTD9xTXJaRvMHYjEeLPA2pFkheNlgPLCkxdvhxhfuM4KGcqSZj2qEnpHisdTVs3BxuQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.7.tgz", + "integrity": "sha512-nU/TRGHHeG81NeLW5DeQT5t6BDUqbpsNQTvef1ld/tqHT+/zTx60/TIhKnmPISTTe++DVo+DLxDmk4rnwHaZVw==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.3", + "@next/eslint-plugin-next": "15.5.7", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -5654,12 +5654,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.3.tgz", - "integrity": "sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", "dependencies": { - "@next/env": "15.5.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -5672,14 +5672,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.3", - "@next/swc-darwin-x64": "15.5.3", - "@next/swc-linux-arm64-gnu": "15.5.3", - "@next/swc-linux-arm64-musl": "15.5.3", - "@next/swc-linux-x64-gnu": "15.5.3", - "@next/swc-linux-x64-musl": "15.5.3", - "@next/swc-win32-arm64-msvc": "15.5.3", - "@next/swc-win32-x64-msvc": "15.5.3", + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", "sharp": "^0.34.3" }, "peerDependencies": { diff --git a/package.json b/package.json index 7396d49d..52fc6ce2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "formik": "^2.4.6", "moment": "^2.30.1", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.0", "react-day-picker": "^9.11.1", "react-dom": "19.1.0", @@ -39,9 +39,9 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "daisyui": "^5.1.12", + "daisyui": "^5.5.8", "eslint": "^9", - "eslint-config-next": "15.5.3", + "eslint-config-next": "^15.5.7", "husky": "^9.1.7", "prettier": "^3.6.2", "tailwindcss": "^4", diff --git a/src/app/closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx new file mode 100644 index 00000000..7220dfa1 --- /dev/null +++ b/src/app/closing/detail/layout.tsx @@ -0,0 +1,11 @@ +import SuspenseHelper from '@/components/helper/SuspenseHelper'; + +const Layout = ({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/closing/detail/page.tsx b/src/app/closing/detail/page.tsx new file mode 100644 index 00000000..6225b8dd --- /dev/null +++ b/src/app/closing/detail/page.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +import ClosingDetail from '@/components/pages/closing/ClosingDetail'; + +import { ClosingApi } from '@/services/api/closing'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; + +const ClosingDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const closingId = searchParams.get('closingId'); + + const { data: closing, isLoading: isLoadingClosing } = useSWR( + closingId, + (id: number) => ClosingApi.getGeneralInfo(id) + ); + + if (!closingId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingClosing && (!closing || isResponseError(closing))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingClosing && ( + + )} + + {!isLoadingClosing && isResponseSuccess(closing) && ( + + )} +
+ ); +}; + +export default ClosingDetailPage; diff --git a/src/app/closing/page.tsx b/src/app/closing/page.tsx new file mode 100644 index 00000000..acaa3ee8 --- /dev/null +++ b/src/app/closing/page.tsx @@ -0,0 +1,11 @@ +import ClosingsTable from '@/components/pages/closing/ClosingsTable'; + +const Closing = () => { + return ( +
+ +
+ ); +}; + +export default Closing; diff --git a/src/app/globals.css b/src/app/globals.css index 8f7ca2f4..10b48ad5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -56,6 +56,12 @@ @theme { --font-inter: var(--font-inter); + + --container-sm: 40rem; + --container-md: 48rem; + --container-lg: 64rem; + --container-xl: 80rem; + --container-2xl: 96rem; } html { diff --git a/src/components/MainDrawer.tsx b/src/components/MainDrawer.tsx index 4a3b44b0..3a09c0b1 100644 --- a/src/components/MainDrawer.tsx +++ b/src/components/MainDrawer.tsx @@ -1,161 +1,21 @@ 'use client'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import { usePathname } from 'next/navigation'; import Image from 'next/image'; import { Icon } from '@iconify/react'; import Drawer from '@/components/Drawer'; -import Menu from '@/components/menu/Menu'; -import MenuItem from '@/components/menu/MenuItem'; import Navbar from '@/components/Navbar'; -import Collapse from '@/components/Collapse'; import Button from '@/components/Button'; +import SidebarMenu from '@/components/molecules/SidebarMenu'; import { useUiStore } from '@/stores/ui/ui.store'; import { MAIN_DRAWER_LINKS } from '@/config/constant'; -import { cn } from '@/lib/helper'; - -type CollapseMenuProps = { - title: string; - link: string; - icon: string; - submenu?: CollapseMenuProps[]; - depth?: number; -}; - -const isPathActive = (pathname: string, link?: string) => { - if (!link) return false; - - const splittedPathname = pathname.split('/'); - const splittedLink = link.split('/'); - - const isActiveLinkValid = splittedLink.every((linkChunk, idx) => { - return linkChunk === splittedPathname[idx]; - }); - - return pathname.startsWith(link) && isActiveLinkValid; -}; - -const CollapseMenu = ({ - title, - link, - icon, - submenu, - depth = 0, -}: CollapseMenuProps) => { - const pathname = usePathname(); - const isActive = isPathActive(pathname, link); - const [open, setOpen] = useState(isActive); - - const menuCollapseTitle = ( -
-
- - {title} -
- - -
- ); - - return ( - - -
- {submenu?.map((item, idx) => { - const hasSubmenu = item.submenu && item.submenu.length > 0; - - if (!hasSubmenu) { - return ( - - ); - } - - return ( - - ); - })} -
-
-
- ); -}; - -const MainDrawerMenu = () => { - const pathname = usePathname(); - - return ( - - {MAIN_DRAWER_LINKS.map((item, idx) => { - const hasSubmenu = item.submenu && item.submenu.length > 0; - - if (!hasSubmenu) { - return ( - - ); - } - - return ( - - ); - })} - - ); -}; +import { isPathActive } from '@/lib/helper'; const MainDrawerContent = () => { + const pathname = usePathname(); const { setMainDrawerOpen } = useUiStore(); const closeMainDrawerHandler = () => { @@ -191,7 +51,7 @@ const MainDrawerContent = () => { - + ); }; @@ -216,9 +76,9 @@ const MainDrawer = ({ const hasSubmenu = menu?.submenu && menu?.submenu.length > 0; if (!title) { - title += menu?.title; + title += menu?.text; } else { - title += ' - ' + menu?.title; + title += ' - ' + menu?.text; } if (!hasSubmenu || !menu.submenu) return; diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index e47e480d..43b26d90 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -1,7 +1,9 @@ 'use client'; -import { ReactNode } from 'react'; +import { ChangeEventHandler, ReactNode } from 'react'; + import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; import { cn } from '@/lib/helper'; @@ -17,16 +19,18 @@ const PaginationButton = ({ disabled?: boolean; onClick?: () => void; }) => ( - + ); const EtcPaginationButton = ({ @@ -48,7 +52,7 @@ const EtcPaginationButton = ({ tabIndex={0} role='button' className={cn( - 'join-item btn btn-ghost p-2.5 rounded-lg text-sm font-medium text-gray-500 aspect-square' + 'join-item btn btn-ghost p-2.5 rounded-lg! text-sm font-medium text-gray-500 aspect-square' )} > ... @@ -57,7 +61,7 @@ const EtcPaginationButton = ({
    {pages.map((pageNumber) => (
  • @@ -76,7 +80,7 @@ const EtcPaginationButton = ({ + ); + + const PrevPageButton = () => ( + + ); + + const GoToLastPageButton = () => ( + + ); + + const NextPageButton = () => ( + + ); + + const PageInfo = () => ( + + Page {currentPage} of {totalPages} + + ); return ( -
    -
    - +
    +
    +
    + +
    - {totalPages <= 7 && ( -
    - {range(1, totalPages).map((pageNumber) => ( +
    +
    + +
    + +
    + +
    + + {totalPages <= 7 && + range(1, totalPages).map((pageNumber) => ( pageChangeHandler(pageNumber)} /> ))} -
    - )} - {totalPages > 7 && ( -
    - pageChangeHandler(1)} - /> - - {totalPages >= 2 && - (currentPage <= 3 || currentPage >= totalPages - 2) && ( - pageChangeHandler(2)} - /> - )} - - {totalPages >= 2 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 3 && - (currentPage <= 4 || currentPage >= totalPages - 2) && - currentPage !== totalPages - 2 && ( - pageChangeHandler(3)} - /> - )} - - {totalPages >= 7 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - = totalPages - 1 - ? 4 - : 1 - } - endPage={ - currentPage <= 2 || currentPage >= totalPages - 1 - ? totalPages - 3 - : currentPage === totalPages - 2 - ? totalPages - 4 - : 2 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 3 && - currentPage > 4 && - currentPage < totalPages - 1 && ( - pageChangeHandler(currentPage - 1)} - /> - )} - - {totalPages >= 7 && - currentPage > 3 && - currentPage < totalPages - 2 && ( - - )} - - {totalPages >= 5 && - currentPage > 2 && - currentPage < totalPages - 2 && ( - pageChangeHandler(currentPage + 1)} - /> - )} - - {totalPages >= 5 && - (currentPage <= 2 || currentPage >= totalPages - 2) && ( - pageChangeHandler(totalPages - 2)} - /> - )} - - {totalPages >= 6 && - currentPage > 2 && - currentPage < totalPages - 3 && ( - = 4 - ? currentPage + 2 - : 1 - } - endPage={ - currentPage <= 3 - ? totalPages - 2 - : currentPage >= 4 - ? totalPages - 1 - : 0 - } - onPageItemClick={pageChangeHandler} - /> - )} - - {totalPages >= 6 && - (currentPage <= 3 || currentPage >= totalPages - 3) && ( - pageChangeHandler(totalPages - 1)} - /> - )} - - {totalPages >= 7 && ( + {totalPages > 7 && ( + <> pageChangeHandler(totalPages)} + content={1} + disabled={currentPage === 1} + onClick={() => pageChangeHandler(1)} /> - )} -
    - )} - + +
    + +
    + +
    + +
    +
    + +
    + +
    -
    - +
    +
    + + + + +
    - +
    + + + +
    ); diff --git a/src/components/Table.tsx b/src/components/Table.tsx index b02dd3b5..9feb33e2 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -14,6 +14,7 @@ import { SortingState, OnChangeFn, Row, + HeaderContext, } from '@tanstack/react-table'; import { rankItem } from '@tanstack/match-sorter-utils'; import { Icon } from '@iconify/react'; @@ -31,6 +32,9 @@ interface TableClassNames { tableBodyClassName?: string; bodyRowClassName?: string; bodyColumnClassName?: string; + tableFooterClassName?: string; + footerRowClassName?: string; + footerColumnClassName?: string; paginationClassName?: string; } @@ -38,6 +42,7 @@ export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; + onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; @@ -52,6 +57,9 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + renderFooter?: boolean; + withCheckbox?: boolean; + rowOptions?: number[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -64,28 +72,36 @@ const emptyContentDefaultValue = (
    ); +export const TABLE_DEFAULT_STYLING = { + containerClassName: 'w-full mb-20', + tableWrapperClassName: + 'overflow-x-auto border border-solid border-base-content/10 rounded-lg', + tableClassName: 'font-inter w-full table-auto text-sm font-medium', + tableHeaderClassName: '', + headerRowClassName: '', + headerColumnClassName: + 'px-4 py-3 border-base-content/10 text-base-content/50', + tableBodyClassName: '', + bodyRowClassName: 'border-t border-base-content/10', + bodyColumnClassName: 'px-4 py-3 text-base-content', + paginationClassName: '', + tableFooterClassName: 'font-semibold border-base-content/10', + footerRowClassName: 'bg-base-200 border-t-2 border-base-content/10', + footerColumnClassName: 'p-4 text-base-content whitespace-nowrap', +}; + const Table = ({ data = [], columns = [], pageSize = 10, + onPageSizeChange, totalItems, page, onPageChange, isLoading = false, fuzzySearchValue, onFuzzySearchValueChange, - className = { - containerClassName: '', - tableWrapperClassName: '', - tableClassName: '', - tableHeaderClassName: '', - headerRowClassName: '', - headerColumnClassName: '', - tableBodyClassName: '', - bodyRowClassName: '', - bodyColumnClassName: '', - paginationClassName: '', - }, + className = TABLE_DEFAULT_STYLING, emptyContent = emptyContentDefaultValue, sorting, setSorting, @@ -93,12 +109,20 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + renderFooter = false, + withCheckbox = false, + rowOptions = [10, 20, 50, 100], }: TableProps) => { const isServerSideTable = totalItems !== undefined && page !== undefined && onPageChange !== undefined; + const tableClassNames = { + ...TABLE_DEFAULT_STYLING, + ...className, + }; + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: pageSize, @@ -191,68 +215,106 @@ const Table = ({ }, [pageSize, setPageSize]); return ( -
    -
    - - +
    +
    +
    + {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - + {headerGroup.headers.map((header) => { + const columnRelativeDepth = + header.depth - header.column.depth; + if ( + !header.isPlaceholder && + columnRelativeDepth > 1 && + header.id === header.column.id + ) { + return null; + } + let rowSpan = 1; + if (header.isPlaceholder) { + const leafs = header.getLeafHeaders(); + rowSpan = leafs[leafs.length - 1].depth - header.depth; + } + return ( + - ))} + {header.column.getCanSort() && ( +
    + + +
    + )} + + + ); + })} ))} - + {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( - ))} + + {renderFooter && ( + + {table.getAllLeafColumns().map((column) => ( + + ))} + + )} +
    -
    - {flexRender( - header.column.columnDef.header, - header.getContext() +
    1, + }, + tableClassNames.headerColumnClassName )} + > +
    1, + })} + > + {flexRender( + header.column.columnDef.header, + header.getContext() + )} - {header.column.getCanSort() && ( -
    - - -
    - )} -
    -
    + {!isLoading && flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -262,6 +324,28 @@ const Table = ({
    + {column.columnDef.footer && + flexRender(column.columnDef.footer, { + column, + header: column.columnDef, + table, + } as HeaderContext)} +
    @@ -270,7 +354,7 @@ const Table = ({ emptyContent} {data.length > 0 && table.getRowModel().rows.length > 0 && !isLoading && ( -
    +
    ({ onPrevPage={prevPageClickHandler} onNextPage={nextPageClickHandler} onPageChange={pageChangeHandler} + rowOptions={rowOptions} + onRowChange={onPageSizeChange} />
    )} diff --git a/src/components/helper/RequireAuth.tsx b/src/components/helper/RequireAuth.tsx index dbd4b6bc..119d74cb 100644 --- a/src/components/helper/RequireAuth.tsx +++ b/src/components/helper/RequireAuth.tsx @@ -6,147 +6,9 @@ import useSWRImmutable from 'swr/immutable'; import { useAuth } from '@/services/hooks/useAuth'; import { httpClientFetcher, SWRHttpKey } from '@/services/http/client'; -import { isResponseSuccess } from '@/lib/api-helper'; -import { GetMeResponse } from '@/types/api/api-general'; - -// TODO: delete this later, DONT HARDCODE USER DATA -const DUMMY_USER = { - id: 1, - email: 'admin@mbugroup.id', - npk: '0001', - name: 'Super Admin', - image: null, - created_at: '2025-09-30T03:24:20.899229Z', - updated_at: '2025-09-30T03:24:20.899229Z', - roles: [ - { - id: 1, - key: 'mbu.super_admin', - name: 'MBU Administrator', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - permissions: [ - { - id: 1, - name: 'mbu:purchase:read', - action: 'read', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 2, - name: 'mbu:purchase:create', - action: 'create', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - { - id: 3, - name: 'mbu:purchase:approve', - action: 'approve', - client: { - id: 1, - name: 'PT Mitra Berlian Unggas', - alias: 'MBU', - }, - }, - ], - }, - { - id: 2, - key: 'lti.super_admin', - name: 'LTI Administrator', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - permissions: [ - { - id: 4, - name: 'lti:purchase:read', - action: 'read', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 5, - name: 'lti:purchase:create', - action: 'create', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - { - id: 6, - name: 'lti:purchase:approve', - action: 'approve', - client: { - id: 2, - name: 'PT Lumbung Telur Indonesia', - alias: 'LTI', - }, - }, - ], - }, - { - id: 3, - key: 'manbu.super_admin', - name: 'MANBU Administrator', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - permissions: [ - { - id: 7, - name: 'manbu:purchase:read', - action: 'read', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 8, - name: 'manbu:purchase:create', - action: 'create', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - { - id: 9, - name: 'manbu:purchase:approve', - action: 'approve', - client: { - id: 3, - name: 'PT Mandiri Berlian Unggas', - alias: 'MANBU', - }, - }, - ], - }, - ], -}; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { BaseApiResponse, GetMeResponse } from '@/types/api/api-general'; +import { AxiosError } from 'axios'; interface RequireAuthProps { children?: ReactNode; @@ -156,17 +18,20 @@ const RequireAuth = ({ children }: RequireAuthProps) => { const router = useRouter(); const { setUser, setIsLoadingUser } = useAuth(); - const { data: userResponse, isLoading: isLoadingUserResponse } = - useSWRImmutable( - '/auth/sso/userinfo', - httpClientFetcher, - { - shouldRetryOnError: false, - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshInterval: 0, - } - ); + const { + data: userResponse, + isLoading: isLoadingUserResponse, + error: userErrorResponse, + } = useSWRImmutable< + GetMeResponse & { ok?: boolean }, + AxiosError, + SWRHttpKey + >('/sso/userinfo', httpClientFetcher, { + shouldRetryOnError: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshInterval: 0, + }); useEffect(() => { setIsLoadingUser(isLoadingUserResponse); @@ -175,23 +40,25 @@ const RequireAuth = ({ children }: RequireAuthProps) => { useEffect(() => { if (isResponseSuccess(userResponse)) { setUser(userResponse.data); - } else { - // router.replace(process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string); - // TODO: remove this later, DONT HARDCODE USER DATA - setUser(DUMMY_USER); + } else if ( + isResponseError(userErrorResponse?.response?.data) && + typeof window !== 'undefined' + ) { + router.replace( + `${process.env.NEXT_PUBLIC_SSO_LOGIN_URL as string}?redirect_url=${window.location.href}` + ); } - }, [userResponse, setIsLoadingUser, setUser]); + }, [userResponse, userErrorResponse, setIsLoadingUser, setUser]); - // TODO: uncomment this later - // if (isLoadingUserResponse && !userResponse) { - // return ( - //
    - // - //
    - // ); - // } + if (isLoadingUserResponse && !userResponse && !userErrorResponse) { + return ( +
    + +
    + ); + } - return <>{children}; + return <>{isResponseSuccess(userResponse) && children}; }; export default RequireAuth; diff --git a/src/components/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index f14bc852..32f14f94 100644 --- a/src/components/input/CheckboxInput.tsx +++ b/src/components/input/CheckboxInput.tsx @@ -2,8 +2,9 @@ import { HTMLProps, useEffect, useRef } from 'react'; import { cn } from '@/lib/helper'; +import { Size } from '@/types/theme'; -interface CheckboxInputProps extends HTMLProps { +interface CheckboxInputProps extends Omit, 'size'> { name: string; label?: string; indeterminate?: boolean; @@ -16,6 +17,7 @@ interface CheckboxInputProps extends HTMLProps { isError?: boolean; isValid?: boolean; errorMessage?: string; + size?: Size; } const CheckboxInput = ({ @@ -27,10 +29,19 @@ const CheckboxInput = ({ isValid, isError, errorMessage, + size = 'sm', ...rest }: CheckboxInputProps) => { const ref = useRef(null!); + const checkboxBaseClassName = cn('checkbox cursor-pointer rounded-md', { + 'checkbox-xs': size === 'xs', + 'checkbox-sm': size === 'sm', + 'checkbox-md': size === 'md', + 'checkbox-lg': size === 'lg', + 'checkbox-xl': size === 'xl', + }); + useEffect(() => { if (typeof indeterminate === 'boolean') { ref.current.indeterminate = !rest.checked && indeterminate; @@ -53,7 +64,7 @@ const CheckboxInput = ({ id={name} name={name} className={cn( - 'checkbox rounded-md cursor-pointer', + checkboxBaseClassName, { 'border-error': isError, 'border-success': isValid, diff --git a/src/components/menu/Menu.tsx b/src/components/menu/Menu.tsx index b3981065..ae74717d 100644 --- a/src/components/menu/Menu.tsx +++ b/src/components/menu/Menu.tsx @@ -1,16 +1,32 @@ import { ReactNode } from 'react'; import { cn } from '@/lib/helper'; +import { Size } from '@/types/theme'; interface MenuProps { children?: ReactNode; + size?: Size; + direction?: 'vertical' | 'horizontal'; className?: string; } -const Menu = ({ children, className }: MenuProps) => { - return ( -
      {children}
    - ); +const Menu = ({ + children, + size = 'md', + direction = 'vertical', + className, +}: MenuProps) => { + const menuBaseClassName = cn('menu w-full', { + 'menu-xs': size === 'xs', + 'menu-sm': size === 'sm', + 'menu-md': size === 'md', + 'menu-lg': size === 'lg', + 'menu-xl': size === 'xl', + 'menu-vertical': direction === 'vertical', + 'menu-horizontal': direction === 'horizontal', + }); + + return
      {children}
    ; }; export default Menu; diff --git a/src/components/molecules/SidebarMenu.tsx b/src/components/molecules/SidebarMenu.tsx new file mode 100644 index 00000000..6a217dcc --- /dev/null +++ b/src/components/molecules/SidebarMenu.tsx @@ -0,0 +1,92 @@ +import Link from 'next/link'; +import Menu from '@/components/menu/Menu'; +import { Icon } from '@iconify/react'; +import { cn, isPathActive } from '@/lib/helper'; + +export interface SidebarMenuItem { + type?: 'item' | 'title'; + text: string; + link: string; + icon?: string; + submenu?: SidebarMenuItem[]; +} + +interface SidebarMenuItemProps { + item: SidebarMenuItem; + activeLink: string; +} + +interface SidebarMenuProps { + menu: SidebarMenuItem[]; + activeLink: string; +} + +const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => { + const isItemActive = isPathActive(activeLink, item.link); + + const menuItemWithoutSubmenu = ( +
  • + + {item.icon && } + + {item.text} + +
  • + ); + + if (!item.submenu || item.submenu.length === 0) { + return menuItemWithoutSubmenu; + } + + const menuItemWithSubmenu = ( +
  • +
    + + {item.icon && } + + {item.text} + + +
      + {item.submenu.map((submenuItem, submenuIdx) => ( + + ))} +
    +
    +
  • + ); + + return menuItemWithSubmenu; +}; + +const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => { + return ( + + {menu.map((menuItem, menuIdx) => ( + + ))} + + ); +}; + +export default SidebarMenu; diff --git a/src/components/pages/closing/ClosingDetail.tsx b/src/components/pages/closing/ClosingDetail.tsx new file mode 100644 index 00000000..86f39d18 --- /dev/null +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useMemo, useState } from 'react'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Tabs from '@/components/Tabs'; +import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; + +import { ClosingGeneralInformation } from '@/types/api/closing'; +import ClosingSapronakTabContent from './ClosingSapronakTabContent'; +import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; + +interface ClosingDetailProps { + id: number; + initialValue?: ClosingGeneralInformation; +} + +const ClosingDetail: React.FC = ({ id, initialValue }) => { + const [activeTab, setActiveTab] = useState('sapronak'); + + const closingDetailTabs = useMemo(() => { + const validTabs = [ + { + id: 'sapronak', + label: 'Sapronak', + content: , + }, + { + id: 'perhitunganSapronak', + label: 'Perhitungan Sapronak', + content: , + }, + { + id: 'penjualan', + label: 'Penjualan', + content: 'Penjualan', + }, + { + id: 'overhead', + label: 'Overhead', + content: 'Overhead', + }, + { + id: 'hppEkspedisi', + label: 'HPP Ekspedisi', + content: 'HPP Ekspedisi', + }, + { + id: 'dataProduksi', + label: 'Data Produksi', + content: 'Data Produksi', + }, + { + id: 'keuangan', + label: 'Keuangan', + content: 'Keuangan', + }, + ]; + + return validTabs; + }, [initialValue]); + + return ( + <> +
    +
    + + +

    Detail Closing

    +
    + + + + +
    + + ); +}; + +export default ClosingDetail; diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/ClosingGeneralInformationTable.tsx new file mode 100644 index 00000000..af21497a --- /dev/null +++ b/src/components/pages/closing/ClosingGeneralInformationTable.tsx @@ -0,0 +1,100 @@ +import { ClosingGeneralInformation } from '@/types/api/closing'; + +interface ClosingGeneralInformationProps { + initialValue?: ClosingGeneralInformation; +} + +const ClosingGeneralInformationTable = ({ + initialValue, +}: ClosingGeneralInformationProps) => { + return ( +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Lokasi:{initialValue?.location_name}
    Periode:{initialValue?.period}
    Kategori:{initialValue?.project_category}
    Populasi:{initialValue?.population} Ekor
    Jenis Project:{initialValue?.project_type}
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    +
    +
    + ); +}; + +export default ClosingGeneralInformationTable; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx new file mode 100644 index 00000000..206beb3d --- /dev/null +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'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, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingIncomingSapronak } from '@/types/api/closing'; + +interface ClosingIncomingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingIncomingSapronaksTable = ({ + projectFlockId, +}: ClosingIncomingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/incoming${getTableFilterQueryString()}`, + ClosingApi.getAllIncomingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const incomingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks.data.length > 0 + : false + ); + } + }, [incomingSapronaks, isResponseSuccess]); + + return ( + + +
    Sapronak Masuk
    + + +
+ } + className='w-full!' + titleClassName='w-full p-0!' + > +
+
+
+ +
+
+ + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronaks) && + incomingSapronaks?.data?.length === 0, + }), + }} + /> +
+ + + ); +}; + +export default ClosingIncomingSapronaksTable; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx new file mode 100644 index 00000000..9047e79a --- /dev/null +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'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, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingOutgoingSapronak } from '@/types/api/closing'; + +interface ClosingOutgoingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingOutgoingSapronaksTable = ({ + projectFlockId, +}: ClosingOutgoingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/outgoing${getTableFilterQueryString()}`, + ClosingApi.getAllOutgoingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const outgoingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks.data.length > 0 + : false + ); + } + }, [outgoingSapronaks, isResponseSuccess]); + + return ( + + +
Sapronak Keluar
+ + + + } + className='w-full!' + titleClassName='w-full p-0!' + > +
+
+
+ +
+
+ + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks?.data?.length === 0, + }), + }} + /> +
+
+
+ ); +}; + +export default ClosingOutgoingSapronaksTable; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx new file mode 100644 index 00000000..15e43bbc --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx @@ -0,0 +1,25 @@ +'use client'; + +import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; +import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; +import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; + +interface ClosingSapronakCalculationTabContentProps { + projectFlockId?: number; +} + +const ClosingSapronakCalculationTabContent = ({ + projectFlockId, +}: ClosingSapronakCalculationTabContentProps) => { + return ( +
+ {projectFlockId && ( + <> + + + )} +
+ ); +}; + +export default ClosingSapronakCalculationTabContent; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx new file mode 100644 index 00000000..73c10331 --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -0,0 +1,218 @@ +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; + +interface ClosingSapronakCalculationTableProps { + type?: 'detail'; + projectFlockId: number; +} + +const ClosingSapronakCalculationTable = ({ + type, + projectFlockId, +}: ClosingSapronakCalculationTableProps) => { + const { data: sapronakCalculation, isLoading } = useSWR( + `/closing/sapronak-calculation/${projectFlockId}`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId) + ); + + // Helper function to create columns with footer support + const createColumns = ( + total?: TotalSapronakCalculation + ): ColumnDef[] => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => (props.getValue() as string) || '-', + footer: 'Total', + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_masuk)} +
+ ) + : '', + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_keluar)} +
+ ) + : '', + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => formatNumber(props.getValue() as number), + footer: total + ? () => ( +
+ {formatNumber(total.qty_pakai)} +
+ ) + : '', + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
+ {formatCurrency(total.harga_beli_per_qty)} +
+ ) + : '', + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => formatCurrency(props.getValue() as number), + footer: total + ? () => ( +
+ {formatCurrency(total.total_harga)} +
+ ) + : '', + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => (props.getValue() as string) || '-', + footer: '', + }, + ]; + + // Memoize columns untuk setiap kategori + const docBroilerColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.doc_broiler.total) + : createColumns(), + [sapronakCalculation] + ); + + const ovkColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.ovk.total) + : createColumns(), + [sapronakCalculation] + ); + + const pakanColumns = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createColumns(sapronakCalculation.data?.pakan.total) + : createColumns(), + [sapronakCalculation] + ); + + return ( +
+ {isResponseSuccess(sapronakCalculation) && ( + <> + + + data={sapronakCalculation.data?.doc_broiler.rows ?? []} + columns={docBroilerColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter + /> + + + + + data={sapronakCalculation.data?.ovk.rows ?? []} + columns={ovkColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter + /> + + + + + data={sapronakCalculation.data?.pakan.rows ?? []} + columns={pakanColumns} + className={{ + containerClassName: 'my-4', + }} + renderFooter + /> + + + )} +
+ ); +}; + +export default ClosingSapronakCalculationTable; diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx new file mode 100644 index 00000000..41c7aa05 --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakTabContent.tsx @@ -0,0 +1,26 @@ +'use client'; + +import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; +import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; + +interface ClosingSapronakTableProps { + projectFlockId?: number; +} + +const ClosingSapronakTabContent = ({ + projectFlockId, +}: ClosingSapronakTableProps) => { + return ( +
+ {projectFlockId && ( + <> + + + + + )} +
+ ); +}; + +export default ClosingSapronakTabContent; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx new file mode 100644 index 00000000..91e78c8c --- /dev/null +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -0,0 +1,299 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { LocationApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { ClosingApi } from '@/services/api/closing'; +import { Closing } from '@/types/api/closing'; + +const PROJECT_STATUS_OPTIONS = [ + { + value: 1, + label: 'Pengajuan', + }, + { + value: 2, + label: 'Aktif', + }, +]; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; +}) => { + return ( + + {/* TODO: apply RBAC */} +
+ +
+
+ ); +}; + +const ClosingsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + transactionDate: '', + realizationDate: '', + locationId: '', + projectStatus: '', + userId: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + transactionDate: 'transaction_date', + realizationDate: 'realization_date', + locationId: 'location_id', + projectStatus: 'project_status', + userId: 'user_id', + }, + }); + + const { data: closings, isLoading: isLoadingClosings } = useSWR( + `${ClosingApi.basePath}${getTableFilterQueryString()}`, + ClosingApi.getAllFetcher + ); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const closingsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'location_name', + header: 'Lokasi', + }, + { + accessorKey: 'project_category', + header: 'Kategori', + }, + { + accessorKey: 'period', + header: 'Periode', + }, + { + accessorKey: 'closing_date', + header: 'Periode', + cell: (props) => + formatDate(props.row.original.closing_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'shed_label', + header: 'Jumlah Kandang', + }, + { + accessorKey: 'sales_paid_amount', + header: 'Jumlah Sudah Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_paid_amount)} + + ), + }, + { + accessorKey: 'sales_remaining_amount', + header: 'Jumlah Sisa Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_remaining_amount)} + + ), + }, + { + accessorKey: 'sales_payment_status', + header: 'Status Pembayaran', + }, + { + accessorKey: 'project_status', + header: 'Status', + }, + { + header: 'Aksi', + cell: (props) => { + const currentPageSize = props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + + return ( + <> + {currentPageSize > 3 && ( + + + + )} + + {currentPageSize <= 3 && ( + + + + )} + + ); + }, + }, + ]; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const [selectedLocation, setSelectedLocation] = useState( + null + ); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + updateFilter( + 'locationId', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedProjectStatus, setSelectedProjectStatus] = + useState(null); + + const projectStatusChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedProjectStatus(val as OptionType); + updateFilter( + 'projectStatus', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + return ( + <> +
+
+
+
+ +
+ +
+ + + +
+
+
+ + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + }} + /> +
+ + ); +}; + +export default ClosingsTable; diff --git a/src/components/pages/production/recording/RecordingTable.tsx b/src/components/pages/production/recording/RecordingTable.tsx index 6cf254e7..4a413bc4 100644 --- a/src/components/pages/production/recording/RecordingTable.tsx +++ b/src/components/pages/production/recording/RecordingTable.tsx @@ -370,7 +370,7 @@ const RecordingTable = () => { const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isApproveLoading, setIsApproveLoading] = useState(false); const [isRejectLoading, setIsRejectLoading] = useState(false); - const [approvalNotes, setApprovalNotes] = useState(''); + const [, setApprovalNotes] = useState(''); const singleDeleteModal = useModal(); const approveModal = useModal(); diff --git a/src/components/pages/production/recording/form/RecordingForm.tsx b/src/components/pages/production/recording/form/RecordingForm.tsx index 52297258..43ffc98b 100644 --- a/src/components/pages/production/recording/form/RecordingForm.tsx +++ b/src/components/pages/production/recording/form/RecordingForm.tsx @@ -2924,8 +2924,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => { }, 1000); } } - } catch (error) { - console.error('Error creating recording:', error); + } catch { toast.error( 'Gagal membuat recording. Silakan coba lagi.' ); diff --git a/src/components/pages/production/recording/grading/form/GradingForm.tsx b/src/components/pages/production/recording/grading/form/GradingForm.tsx index 1e91c78d..417c6356 100644 --- a/src/components/pages/production/recording/grading/form/GradingForm.tsx +++ b/src/components/pages/production/recording/grading/form/GradingForm.tsx @@ -173,8 +173,7 @@ const GradingForm = ({ type = 'add', initialValues }: GradingFormProps) => { deleteModal.closeModal(); toast.success(res?.message || 'Successfully delete Grading!'); router.push('/production/recording'); - } catch (err) { - console.error(err); + } catch { setGradingFormErrorMessage('Failed to delete Grading'); } finally { setIsDeleteLoading(false); diff --git a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx index 69c3fd13..791e2592 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx +++ b/src/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm.tsx @@ -241,9 +241,8 @@ const PurchaseOrderStaffApprovalForm = ({ ); formik.setFieldValue('items', updatedPurchaseItems); } - } catch (error) { + } catch { toast.error('Terjadi kesalahan saat menghapus item pembelian'); - console.error('Delete item error:', error); } }, [ initialValues?.id, diff --git a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx index c8684b2a..396ce7bb 100644 --- a/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx +++ b/src/components/pages/purchase/form/request/PurchaseRequestForm.tsx @@ -28,7 +28,6 @@ import { AreaApi, LocationApi, WarehouseApi, - ProductApi, } from '@/services/api/master-data'; import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; diff --git a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx index d7497d7e..36aea9c7 100644 --- a/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderInvoice.tsx @@ -12,6 +12,7 @@ import { pdf, } from '@react-pdf/renderer'; import { Icon } from '@iconify/react'; +import toast from 'react-hot-toast'; import Button from '@/components/Button'; import { Purchase } from '@/types/api/purchase/purchase'; @@ -251,7 +252,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { const handleDownloadPDF = async () => { if (!purchaseData) { - alert('No purchase order data available'); + toast.error('No purchase order data available'); return; } @@ -502,9 +503,8 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => { link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); - } catch (error) { - console.error('Error generating PDF:', error); - alert('Failed to generate PDF. Please try again.'); + } catch { + toast.error('Failed to generate PDF. Please try again.'); } finally { setIsGeneratingPDF(false); } diff --git a/src/config/constant.ts b/src/config/constant.ts index 6eb9cef4..8f4a9b46 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,154 +1,125 @@ -type MAIN_DRAWER_MENU = { - title: string; - link: string; - icon: string; - submenu?: MAIN_DRAWER_MENU[]; -}; +import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; -export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [ +export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ { - title: 'Dashboard', + text: 'Dashboard', link: '/dashboard', - icon: 'gg:chart', + icon: 'heroicons-outline:chart-bar-square', }, - { - title: 'Produksi', + text: 'Produksi', link: '/production', - icon: 'material-symbols:conveyor-belt-outline-rounded', + icon: 'heroicons-outline:wrench-screwdriver', submenu: [ { - title: 'List Flock', + text: 'Daftar Flock', link: '/production/project-flock', - icon: 'material-symbols:list-alt-add-outline-rounded', }, - // { // DI HILANGKAN PADA VERSI REFACTORING - // title: 'Chick In', - // link: '/production/chickin', - // icon: 'mdi:home-import-outline', - // }, { - title: 'Recording', + text: 'Recording', link: '/production/recording', - icon: 'mdi:clipboard-text', }, { - title: 'Transfer ke Laying', + text: 'Transfer to Laying', link: '/production/transfer-to-laying', - icon: 'streamline:transfer-van', }, ], }, - { - title: 'Pembelian', + text: 'Pembelian', link: '/purchase', - icon: 'gg:shopping-cart', + icon: 'heroicons-outline:shopping-cart', }, - { - title: 'Penjualan', + text: 'Penjualan', link: '/marketing', - icon: 'mdi:attach-money', + icon: 'heroicons-outline:currency-dollar', }, - { - title: 'Biaya Operasional', + text: 'Biaya Operasional', link: '/expense', - icon: 'uil:wallet', + icon: 'heroicons:wallet', }, { - title: 'Persediaan', + text: 'Closing', + link: '/closing', + icon: 'heroicons-outline:presentation-chart-bar', + }, + { + text: 'Persediaan', link: '/inventory', - icon: 'mdi:warehouse', + icon: 'heroicons-outline:folder', submenu: [ { - title: 'Produk', + text: 'Produk', link: '/inventory/product', - icon: 'mdi:package-variant-closed', }, { - title: 'Penyesuaian Stok', + text: 'Penyesuaian Stok', link: '/inventory/adjustment', - icon: 'mdi:database-edit', }, { - title: 'Transfer Stok', + text: 'Transfer Stok', link: '/inventory/movement', - icon: 'mdi:swap-horizontal', }, ], }, - { - title: 'Master Data', + text: 'Master Data', link: '/master-data', - icon: 'majesticons:data-line', + icon: 'heroicons-outline:circle-stack', submenu: [ { - title: 'Product', + text: 'Produk', link: '/master-data/product', - icon: 'fluent-mdl2:product-variant', }, { - title: 'Product Category', + text: 'Kategori Produk', link: '/master-data/product-category', - icon: 'carbon:categories', }, { - title: 'Bank', + text: 'Bank', link: '/master-data/bank', - icon: 'mdi:bank-outline', }, { - title: 'Area', + text: 'Area', link: '/master-data/area', - icon: 'majesticons:map-marker-area-line', }, { - title: 'Location', + text: 'Lokasi', link: '/master-data/location', - icon: 'mingcute:location-line', }, { - title: 'Kandang', + text: 'Kandang', link: '/master-data/kandang', - icon: 'mdi:farm-home-outline', }, { - title: 'Warehouse', + text: 'Warehouse', link: '/master-data/warehouse', - icon: 'hugeicons:warehouse', }, { - title: 'Customer', + text: 'Customer', link: '/master-data/customer', - icon: 'ix:customer', }, { - title: 'UOM', + text: 'UOM', link: '/master-data/uom', - icon: 'lsicon:measure-outline', }, { - title: 'Non-Stock', + text: 'Non-Stock', link: '/master-data/nonstock', - icon: 'fluent:box-32-regular', }, { - title: 'FCR', + text: 'FCR', link: '/master-data/fcr', - icon: 'fluent:food-chicken-leg-16-regular', }, { - title: 'Supplier', + text: 'Supplier', link: '/master-data/supplier', - icon: 'material-symbols:add-business-outline-rounded', }, { - title: 'Flock', + text: 'Flock', link: '/master-data/flock', - icon: 'material-symbols:raven-outline-rounded', }, ], }, diff --git a/src/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts new file mode 100644 index 00000000..8ebb0164 --- /dev/null +++ b/src/dummy/closing.dummy.ts @@ -0,0 +1,984 @@ +/** + * Dummy Data untuk Closing API + * + * File ini berisi dummy data untuk testing API Closing sebelum backend siap. + * + * Struktur data mengikuti tipe yang didefinisikan di @/types/api/closing.d.ts + * + * @example + * // 1. Menggunakan getAllFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * '/closings', + * ClosingApi.getAllFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of Closing objects + * } + * + * @example + * // 2. Menggunakan getSingle: + * import { ClosingApi } from '@/services/api/closing'; + * + * const response = await ClosingApi.getSingle(1); + * if (response?.status === 'success') { + * console.log(response.data); // Single Closing object + * } else if (response?.status === 'error') { + * console.error(response.message); // Error message + * } + * + * @example + * // 3. Menggunakan getGeneralInfo dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const closingId = 1; + * const { data, error, isLoading } = useSWR( + * closingId, + * (id: number) => ClosingApi.getGeneralInfo(id) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // ClosingGeneralInformation object + * } + * + * @example + * // 4. Menggunakan getAllIncomingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/incoming`, + * ClosingApi.getAllIncomingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingIncomingSapronak + * } + * + * @example + * // 5. Menggunakan getAllOutgoingSapronakFetcher dengan SWR: + * import useSWR from 'swr'; + * import { ClosingApi } from '@/services/api/closing'; + * + * const { data, error, isLoading } = useSWR( + * `${ClosingApi.basePath}/1/sapronak/outgoing`, + * ClosingApi.getAllOutgoingSapronakFetcher.bind(ClosingApi) + * ); + * + * if (data?.status === 'success') { + * console.log(data.data); // Array of ClosingOutgoingSapronak + * } + * + * @see {@link /home/sweetpotet/Documents/projects/lti-web-client/src/types/api/closing.d.ts} + */ + +import { format } from 'date-fns'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingSapronakCalculation, +} from '@/types/api/closing'; +import { CreatedUser, BaseApiResponse } from '@/types/api/api-general'; + +// Waktu saat ini untuk created_at/updated_at +const now = format(new Date(), 'yyyy-MM-dd HH:mm:ss'); +const today = format(new Date(), 'yyyy-MM-dd'); +const yesterday = format( + new Date().setDate(new Date().getDate() - 1), + 'yyyy-MM-dd' +); +const lastWeek = format( + new Date().setDate(new Date().getDate() - 7), + 'yyyy-MM-dd' +); +const lastMonth = format( + new Date().setMonth(new Date().getMonth() - 1), + 'yyyy-MM-dd' +); + +// ====================== +// 👤 Created User +// ====================== +export const createdUser: CreatedUser = { + id: 1, + id_user: 1, + email: 'admin@example.com', + name: 'Admin Utama', +}; + +// ====================== +// 📊 Closing Dummy Data +// ====================== +export const dummyClosings: Closing[] = [ + // 1. Closing dengan status Pengajuan - GROWING + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. Closing dengan status Aktif - LAYING + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. Closing dengan status Selesai - GROWING + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. Closing dengan status Aktif - LAYING + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. Closing dengan status Selesai - GROWING + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. Closing dengan status Pengajuan - LAYING + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, // Belum ada tanggal closing + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. Closing dengan status Aktif - GROWING + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. Closing dengan status Selesai - LAYING + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📊 Closing General Information Dummy Data +// ====================== +export const dummyClosingGeneralInformations: ClosingGeneralInformation[] = [ + // 1. General Info - GROWING - Pengajuan + { + id: 1, + location_id: 1, + location_name: 'Farm Sukajadi', + project_category: 'GROWING', + period: 1, + closing_date: today, + shed_label: 'Kandang A1, A2, A3', + shed_count: 3, + sales_paid_amount: 150000000, + sales_remaining_amount: 50000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Pengajuan', + flock_id: 101, + project_type: 'GROWING', + population: 15000, + active_house_count: 3, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 2. General Info - LAYING - Aktif + { + id: 2, + location_id: 2, + location_name: 'Farm Cihampelas', + project_category: 'LAYING', + period: 2, + closing_date: yesterday, + shed_label: 'Kandang B1, B2', + shed_count: 2, + sales_paid_amount: 200000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Aktif', + flock_id: 102, + project_type: 'LAYING', + population: 10000, + active_house_count: 2, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 3. General Info - GROWING - Selesai + { + id: 3, + location_id: 3, + location_name: 'Farm Pasteur', + project_category: 'GROWING', + period: 3, + closing_date: lastWeek, + shed_label: 'Kandang C1, C2, C3, C4', + shed_count: 4, + sales_paid_amount: 300000000, + sales_remaining_amount: 25000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Selesai', + flock_id: 103, + project_type: 'GROWING', + population: 20000, + active_house_count: 4, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastWeek, + }, + + // 4. General Info - LAYING - Aktif + { + id: 4, + location_id: 4, + location_name: 'Farm Setiabudi', + project_category: 'LAYING', + period: 1, + closing_date: today, + shed_label: 'Kandang D1', + shed_count: 1, + sales_paid_amount: 75000000, + sales_remaining_amount: 75000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Aktif', + flock_id: 104, + project_type: 'LAYING', + population: 5000, + active_house_count: 1, + closing_status: 'In Progress', + created_user: createdUser, + created_at: yesterday, + updated_at: now, + }, + + // 5. General Info - GROWING - Selesai + { + id: 5, + location_id: 5, + location_name: 'Farm Dago', + project_category: 'GROWING', + period: 4, + closing_date: lastMonth, + shed_label: 'Kandang E1, E2, E3, E4, E5', + shed_count: 5, + sales_paid_amount: 500000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 105, + project_type: 'GROWING', + population: 25000, + active_house_count: 5, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, + + // 6. General Info - LAYING - Pengajuan + { + id: 6, + location_id: 6, + location_name: 'Farm Lembang', + project_category: 'LAYING', + period: 2, + closing_date: undefined, + shed_label: 'Kandang F1, F2', + shed_count: 2, + sales_paid_amount: 0, + sales_remaining_amount: 180000000, + sales_payment_status: 'Belum Lunas', + project_status: 'Pengajuan', + flock_id: 106, + project_type: 'LAYING', + population: 12000, + active_house_count: 2, + closing_status: 'Draft', + created_user: createdUser, + created_at: now, + updated_at: now, + }, + + // 7. General Info - GROWING - Aktif + { + id: 7, + location_id: 7, + location_name: 'Farm Ciwidey', + project_category: 'GROWING', + period: 1, + closing_date: yesterday, + shed_label: 'Kandang G1, G2, G3', + shed_count: 3, + sales_paid_amount: 120000000, + sales_remaining_amount: 30000000, + sales_payment_status: 'Sebagian Lunas', + project_status: 'Aktif', + flock_id: 107, + project_type: 'GROWING', + population: 18000, + active_house_count: 3, + closing_status: 'In Progress', + created_user: createdUser, + created_at: lastWeek, + updated_at: yesterday, + }, + + // 8. General Info - LAYING - Selesai + { + id: 8, + location_id: 8, + location_name: 'Farm Bandung Timur', + project_category: 'LAYING', + period: 3, + closing_date: lastMonth, + shed_label: 'Kandang H1, H2, H3, H4, H5, H6', + shed_count: 6, + sales_paid_amount: 600000000, + sales_remaining_amount: 0, + sales_payment_status: 'Lunas', + project_status: 'Selesai', + flock_id: 108, + project_type: 'LAYING', + population: 30000, + active_house_count: 6, + closing_status: 'Completed', + created_user: createdUser, + created_at: lastMonth, + updated_at: lastMonth, + }, +]; + +// ====================== +// 📦 Incoming Sapronak Dummy Data +// ====================== +export const dummyIncomingSapronaks: ClosingIncomingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'IN-2025-001', + transaction_type: 'Pembelian', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang A1', + quantity: 5000, + unit: 'Ekor', + formatted_quantity: '5,000 Ekor', + notes: 'DOC berkualitas tinggi dari supplier terpercaya', + }, + { + id: 2, + date: yesterday, + reference_number: 'IN-2025-002', + transaction_type: 'Transfer Masuk', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Gudang Area Bandung', + destination_warehouse: 'Kandang B1', + quantity: 100, + unit: 'Sak', + formatted_quantity: '100 Sak (5,000 Kg)', + notes: 'Pakan starter untuk periode awal', + }, + { + id: 3, + date: lastWeek, + reference_number: 'IN-2025-003', + transaction_type: 'Pembelian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Supplier Medion', + destination_warehouse: 'Gudang Farmasi', + quantity: 50, + unit: 'Botol', + formatted_quantity: '50 Botol', + notes: 'Vitamin untuk meningkatkan daya tahan tubuh', + }, + { + id: 4, + date: today, + reference_number: 'IN-2025-004', + transaction_type: 'Pembelian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Gudang Pusat', + destination_warehouse: 'Kandang C1', + quantity: 200, + unit: 'Sak', + formatted_quantity: '200 Sak (10,000 Kg)', + notes: 'Pakan finisher untuk periode akhir', + }, + { + id: 5, + date: yesterday, + reference_number: 'IN-2025-005', + transaction_type: 'Transfer Masuk', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Area Jakarta', + destination_warehouse: 'Gudang Farmasi', + quantity: 30, + unit: 'Box', + formatted_quantity: '30 Box', + notes: 'Antibiotik untuk pencegahan penyakit', + }, +]; + +// ====================== +// 📤 Outgoing Sapronak Dummy Data +// ====================== +export const dummyOutgoingSapronaks: ClosingOutgoingSapronak[] = [ + { + id: 1, + date: today, + reference_number: 'OUT-2025-001', + transaction_type: 'Pemakaian', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang A1', + destination_warehouse: 'Konsumsi Kandang A1', + quantity: 50, + unit: 'Sak', + formatted_quantity: '50 Sak (2,500 Kg)', + notes: 'Pemakaian pakan harian periode starter', + }, + { + id: 2, + date: yesterday, + reference_number: 'OUT-2025-002', + transaction_type: 'Transfer Keluar', + product_name: 'DOC Broiler Cobb 500', + product_category: 'DOC', + product_sub_category: 'DOC Broiler', + source_warehouse: 'Kandang B1', + destination_warehouse: 'Kandang B2', + quantity: 1000, + unit: 'Ekor', + formatted_quantity: '1,000 Ekor', + notes: 'Transfer DOC ke kandang baru', + }, + { + id: 3, + date: lastWeek, + reference_number: 'OUT-2025-003', + transaction_type: 'Pemakaian', + product_name: 'Vitamin B Complex', + product_category: 'OVK', + product_sub_category: 'Vitamin', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 10, + unit: 'Botol', + formatted_quantity: '10 Botol', + notes: 'Pemberian vitamin untuk meningkatkan kesehatan', + }, + { + id: 4, + date: today, + reference_number: 'OUT-2025-004', + transaction_type: 'Pemakaian', + product_name: 'Pakan Finisher BR-2', + product_category: 'Pakan', + product_sub_category: 'Finisher', + source_warehouse: 'Kandang C1', + destination_warehouse: 'Konsumsi Kandang C1', + quantity: 80, + unit: 'Sak', + formatted_quantity: '80 Sak (4,000 Kg)', + notes: 'Pemakaian pakan harian periode finisher', + }, + { + id: 5, + date: yesterday, + reference_number: 'OUT-2025-005', + transaction_type: 'Pemakaian', + product_name: 'Antibiotik Enrofloxacin', + product_category: 'OVK', + product_sub_category: 'Obat', + source_warehouse: 'Gudang Farmasi', + destination_warehouse: 'Konsumsi Kandang D1', + quantity: 5, + unit: 'Box', + formatted_quantity: '5 Box', + notes: 'Pengobatan untuk ayam yang sakit', + }, + { + id: 6, + date: lastWeek, + reference_number: 'OUT-2025-006', + transaction_type: 'Transfer Keluar', + product_name: 'Pakan Starter BR-1', + product_category: 'Pakan', + product_sub_category: 'Starter', + source_warehouse: 'Kandang E1', + destination_warehouse: 'Kandang E2', + quantity: 30, + unit: 'Sak', + formatted_quantity: '30 Sak (1,500 Kg)', + notes: 'Transfer pakan antar kandang', + }, +]; + +// ====================== +// 📊 Perhitungan Sapronak Dummy Data +// ====================== +export const dummySapronakCalculation: ClosingSapronakCalculation = { + // DOC Broiler Calculation + doc_broiler: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-001', + qty_masuk: 5000, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 40000000, + keterangan: 'Pembelian DOC dari supplier', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'OUT-2025-002', + qty_masuk: 0, + qty_keluar: 1000, + qty_pakai: 0, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 8000000, + keterangan: 'Transfer DOC ke kandang lain', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'USE-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'DOC Broiler Cobb 500', + kategori_produk: 'DOC Broiler', + harga_beli_per_qty: 8000, + total_harga: 400000, + keterangan: 'Mortalitas DOC', + }, + ], + total: { + label: 'Total DOC Broiler', + qty_masuk: 5000, + qty_keluar: 1000, + qty_pakai: 50, + harga_beli_per_qty: 8000, + total_harga: 48400000, + }, + }, + + // OVK Calculation + ovk: { + rows: [ + { + id: 1, + tanggal: today, + no_referensi: 'IN-2025-003', + qty_masuk: 50, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 7500000, + keterangan: 'Pembelian vitamin', + }, + { + id: 2, + tanggal: yesterday, + no_referensi: 'IN-2025-005', + qty_masuk: 30, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 7500000, + keterangan: 'Pembelian antibiotik', + }, + { + id: 3, + tanggal: lastWeek, + no_referensi: 'OUT-2025-003', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 10, + uraian: 'Vitamin B Complex', + kategori_produk: 'Vitamin', + harga_beli_per_qty: 150000, + total_harga: 1500000, + keterangan: 'Pemakaian vitamin', + }, + { + id: 4, + tanggal: yesterday, + no_referensi: 'OUT-2025-005', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 5, + uraian: 'Antibiotik Enrofloxacin', + kategori_produk: 'Obat', + harga_beli_per_qty: 250000, + total_harga: 1250000, + keterangan: 'Pemakaian antibiotik', + }, + ], + total: { + label: 'Total OVK', + qty_masuk: 80, + qty_keluar: 0, + qty_pakai: 15, + harga_beli_per_qty: 200000, + total_harga: 17750000, + }, + }, + + // Pakan Calculation + pakan: { + rows: [ + { + id: 1, + tanggal: yesterday, + no_referensi: 'IN-2025-002', + qty_masuk: 100, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 45000000, + keterangan: 'Pembelian pakan starter', + }, + { + id: 2, + tanggal: today, + no_referensi: 'IN-2025-004', + qty_masuk: 200, + qty_keluar: 0, + qty_pakai: 0, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 96000000, + keterangan: 'Pembelian pakan finisher', + }, + { + id: 3, + tanggal: today, + no_referensi: 'OUT-2025-001', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 50, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 22500000, + keterangan: 'Pemakaian pakan starter', + }, + { + id: 4, + tanggal: today, + no_referensi: 'OUT-2025-004', + qty_masuk: 0, + qty_keluar: 0, + qty_pakai: 80, + uraian: 'Pakan Finisher BR-2', + kategori_produk: 'Finisher', + harga_beli_per_qty: 480000, + total_harga: 38400000, + keterangan: 'Pemakaian pakan finisher', + }, + { + id: 5, + tanggal: lastWeek, + no_referensi: 'OUT-2025-006', + qty_masuk: 0, + qty_keluar: 30, + qty_pakai: 0, + uraian: 'Pakan Starter BR-1', + kategori_produk: 'Starter', + harga_beli_per_qty: 450000, + total_harga: 13500000, + keterangan: 'Transfer pakan ke kandang lain', + }, + ], + total: { + label: 'Total Pakan', + qty_masuk: 300, + qty_keluar: 30, + qty_pakai: 130, + harga_beli_per_qty: 465000, + total_harga: 215400000, + }, + }, +}; + +// ====================== +// 🔧 Dummy API Response Functions +// ====================== + +/** + * Dummy implementation for getAllFetcher + * Returns all closing records + */ +export const dummyGetAllFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: Closing[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 500)); + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: dummyClosings, + }; +}; + +/** + * Dummy implementation for getSingle + * Returns a single closing by ID + */ +export const dummyGetSingle = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closing = dummyClosings.find((c) => c.id === id); + + if (!closing) { + return { + code: 404, + status: 'error', + message: `Closing dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing berhasil diambil', + data: closing, + }; +}; + +/** + * Dummy implementation for getAllIncomingSapronakFetcher + * Returns all incoming sapronak records + */ +export const dummyGetAllIncomingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingIncomingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak masuk berhasil diambil', + data: dummyIncomingSapronaks, + }; +}; + +/** + * Dummy implementation for getAllOutgoingSapronakFetcher + * Returns all outgoing sapronak records + */ +export const dummyGetAllOutgoingSapronakFetcher = async (): Promise<{ + code: number; + status: 'success'; + message: string; + data: ClosingOutgoingSapronak[]; +}> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data sapronak keluar berhasil diambil', + data: dummyOutgoingSapronaks, + }; +}; + +/** + * Dummy implementation for getGeneralInfo + * Returns closing general information by ID + */ +export const dummyGetGeneralInfo = async ( + id: number +): Promise | undefined> => { + await new Promise((resolve) => setTimeout(resolve, 300)); + const closingInfo = dummyClosingGeneralInformations.find((c) => c.id == id); + + if (!closingInfo) { + return { + code: 404, + status: 'error', + message: `Closing general information dengan ID ${id} tidak ditemukan`, + }; + } + + return { + code: 200, + status: 'success', + message: 'Data closing general information berhasil diambil', + data: closingInfo, + }; +}; + +/** + * Dummy implementation for getPerhitunganSapronak + * Returns sapronak calculation data + */ +export const dummyGetPerhitunganSapronak = async ( + id: number +): Promise< + | { + code: number; + status: 'success'; + message: string; + data: ClosingSapronakCalculation; + } + | undefined +> => { + await new Promise((resolve) => setTimeout(resolve, 400)); + return { + code: 200, + status: 'success', + message: 'Data perhitungan sapronak berhasil diambil', + data: dummySapronakCalculation, + }; +}; diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 1438c98f..c69f610f 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -10,6 +10,8 @@ export const sleep = (ms: number = 1000) => new Promise((resolve) => setTimeout(resolve, ms)); export const formatDate = (date: moment.MomentInput, format?: string) => { + if (!date) return '-'; + return moment(date).format(format); }; @@ -127,3 +129,16 @@ export const convertRowSelectionObjToArr = ( return result; }; + +export const isPathActive = (pathname: string, link?: string) => { + if (!link) return false; + + const splittedPathname = pathname.split('/'); + const splittedLink = link.split('/'); + + const isActiveLinkValid = splittedLink.every((linkChunk, idx) => { + return linkChunk === splittedPathname[idx]; + }); + + return pathname.startsWith(link) && isActiveLinkValid; +}; diff --git a/src/services/api/closing.ts b/src/services/api/closing.ts new file mode 100644 index 00000000..9514f6a3 --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,152 @@ +import axios from 'axios'; + +import { BaseApiService } from '@/services/api/base'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingSapronakCalculation, +} from '@/types/api/closing'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { + dummyGetAllFetcher, + dummyGetSingle, + dummyGetAllIncomingSapronakFetcher, + dummyGetAllOutgoingSapronakFetcher, + dummyGetGeneralInfo, + dummyGetPerhitunganSapronak, +} from '@/dummy/closing.dummy'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; + +export class ClosingApiService extends BaseApiService { + constructor(basePath: string) { + super(basePath); + } + + async getAllFetcher(endpoint: string): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllFetcher(); + + // Uncomment this when backend is ready + return await httpClientFetcher>(endpoint); + } + + async getSingle(id: number): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetSingle(id); + // } catch (error) { + // if (axios.isAxiosError>(error)) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const getSinglePath = `${this.basePath}/${id}`; + const getSingleRes = + await httpClient>(getSinglePath); + return getSingleRes; + } catch (error) { + if (axios.isAxiosError>(error)) { + return error.response?.data; + } + return undefined; + } + } + + async getAllIncomingSapronakFetcher( + endpoint: string + ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllIncomingSapronakFetcher(); + + // Uncomment this when backend is ready + return await httpClientFetcher>( + endpoint + ); + } + + async getAllOutgoingSapronakFetcher( + endpoint: string + ): Promise> { + // TODO: Remove this block when backend is ready + // return await dummyGetAllOutgoingSapronakFetcher(); + + // Uncomment this when backend is ready + return await httpClientFetcher>( + endpoint + ); + } + + async getGeneralInfo( + id: number + ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetGeneralInfo(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const getGeneralInfoPath = `${this.basePath}/${id}`; + const getGeneralInfoRes = + await httpClient>( + getGeneralInfoPath + ); + return getGeneralInfoRes; + } catch (error) { + if ( + axios.isAxiosError>(error) + ) { + return error.response?.data; + } + return undefined; + } + } + + async getPerhitunganSapronak( + id: number + ): Promise | undefined> { + // TODO: Remove this block when backend is ready + // try { + // return await dummyGetPerhitunganSapronak(id); + // } catch (error) { + // if ( + // axios.isAxiosError>(error) + // ) { + // return error.response?.data; + // } + // return undefined; + // } + + // Uncomment this when backend is ready + try { + const path = `${this.basePath}/${id}/perhitungan_sapronak`; + return await httpClient>( + path, + { + method: 'GET', + } + ); + } catch (error) { + if ( + axios.isAxiosError>(error) + ) { + return error.response?.data; + } + return undefined; + } + } +} + +export const ClosingApi = new ClosingApiService('/closings'); diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css index fc87399f..8eca2c82 100644 --- a/src/styles/daisyui.css +++ b/src/styles/daisyui.css @@ -1,4 +1,9 @@ @layer utilities { + .menu { + --menu-active-fg: var(--color-primary); + --menu-active-bg: transparent; + } + .step.step-success::before { --step-bg: var(--color-success); --step-fg: var(--color-success-content); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts new file mode 100644 index 00000000..0c4c84df --- /dev/null +++ b/src/types/api/closing.d.ts @@ -0,0 +1,91 @@ +import { Area } from '@/types/api/master-data/area'; +import { Fcr } from '@/types/api/master-data/fcr'; +import { Flock } from '@/types/api/master-data/flock'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Location } from '@/types/api/master-data/location'; +import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; + +export type BaseClosing = { + id: number; + location_id: number; + location_name: string; + project_category: 'GROWING' | 'LAYING'; + period: number; + closing_date?: string; + shed_label: string; + shed_count: number; + sales_paid_amount: number; + sales_remaining_amount: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; +}; + +export type Closing = BaseMetadata & BaseClosing; + +export type BaseClosingGeneralInformation = BaseClosing & { + flock_id: number; + period: number; + project_type: 'GROWING' | 'LAYING'; + population: number; + active_house_count: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; + closing_status: string; +}; + +export type ClosingGeneralInformation = BaseMetadata & + BaseClosingGeneralInformation; + +export type ClosingIncomingSapronak = { + id: number; + date: string; + reference_number: string; + transaction_type: string; + product_name: string; + product_category: string; + product_sub_category: string; + source_warehouse: string; + destination_warehouse: string; + quantity: number; + unit: string; + formatted_quantity: string; + notes: string; +}; + +export type ClosingOutgoingSapronak = ClosingIncomingSapronak; + +// ====== PERHITUNGAN SAPRONAK ====== + +export type RowSapronakCalculation = { + id: number; + tanggal: string; + no_referensi: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + uraian: string; + kategori_produk: string; + harga_beli_per_qty: number; + total_harga: number; + keterangan: string; +}; + +export type TotalSapronakCalculation = { + label: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + harga_beli_per_qty: number; + total_harga: number; +}; + +export type ClosingSapronakCalculationItem = { + rows: RowSapronakCalculation[]; + total: TotalSapronakCalculation; +}; + +export type ClosingSapronakCalculation = { + doc_broiler: ClosingSapronakCalculationItem; + ovk: ClosingSapronakCalculationItem; + pakan: ClosingSapronakCalculationItem; +}; diff --git a/src/types/theme.d.ts b/src/types/theme.d.ts index f83750e4..dcd9e13f 100644 --- a/src/types/theme.d.ts +++ b/src/types/theme.d.ts @@ -1,4 +1,4 @@ -type Color = +export type Color = | 'primary' | 'secondary' | 'accent' @@ -9,4 +9,4 @@ type Color = | 'error' | 'none'; -export { Color }; +export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';