diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91da62b9..c37bfd35 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,6 @@ deploy:dev: environment: name: development url: https://dev-lti-erp.mbugroup.id - # ====== PRODUCTION ====== # build:production: # <<: *build_template @@ -163,4 +162,3 @@ deploy:dev: # environment: # name: production - diff --git a/package-lock.json b/package-lock.json index ec1316ae..01bff9ef 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..e1f92aaf 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 e50e020d..3fe7db88 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -43,6 +43,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..970c5bc1 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -38,6 +38,7 @@ export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; + onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; @@ -52,6 +53,8 @@ export interface TableProps { rowSelection?: Record; setRowSelection?: OnChangeFn>; enableRowSelection?: boolean | ((row: Row) => boolean); + withCheckbox?: boolean; + rowOptions?: number[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -64,28 +67,32 @@ const emptyContentDefaultValue = (
    ); +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 text-base-content/50', + tableBodyClassName: '', + bodyRowClassName: 'border-t border-t-base-content/10', + bodyColumnClassName: 'px-4 py-3 text-base-content', + paginationClassName: '', +}; + 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 +100,19 @@ const Table = ({ rowSelection, setRowSelection, enableRowSelection, + 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,12 +205,15 @@ const Table = ({ }, [pageSize, setPageSize]); return ( -
    -
    - - +
    +
    +
    + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( - + {table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( -
    ({ header.column.getCanSort() ? 'cursor-pointer select-none' : '', - className.headerColumnClassName + { + 'first:w-9 first:pr-0': withCheckbox, + }, + tableClassNames.headerColumnClassName )} >
    @@ -216,12 +236,13 @@ const Table = ({ )} {header.column.getCanSort() && ( -
    +
    ({ )} /> ({ ))}
    + {!isLoading && flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -270,7 +298,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/input/CheckboxInput.tsx b/src/components/input/CheckboxInput.tsx index fb0c95c7..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 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..147b3fbd --- /dev/null +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -0,0 +1,95 @@ +'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'; + +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: 'Perhitungan Sapronak', + }, + { + 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..53e45710 --- /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${getTableFilterQueryString()}&type=incoming`, + 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..5662cff1 --- /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${getTableFilterQueryString()}&type=outgoing`, + 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/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/config/constant.ts b/src/config/constant.ts index dc36025b..bad4a802 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -1,155 +1,121 @@ -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: 'Product', - // 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/lib/helper.ts b/src/lib/helper.ts index 2c66e1cf..fe67afef 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); }; @@ -119,3 +121,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..041108d0 --- /dev/null +++ b/src/services/api/closing.ts @@ -0,0 +1,54 @@ +import axios from 'axios'; + +import { BaseApiService } from '@/services/api/base'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, +} from '@/types/api/closing'; +import { httpClient, httpClientFetcher } from '@/services/http/client'; +import { BaseApiResponse } from '@/types/api/api-general'; + +export class ClosingApiService extends BaseApiService { + constructor(basePath: string) { + super(basePath); + } + + async getAllIncomingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getAllOutgoingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getGeneralInfo(id: number) { + 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; + } + } +} + +export const ClosingApi = new ClosingApiService('/closing'); 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..3f7ba816 --- /dev/null +++ b/src/types/api/closing.d.ts @@ -0,0 +1,55 @@ +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; 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';