diff --git a/package-lock.json b/package-lock.json index d73a1b22..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.7", + "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", @@ -1088,9 +1088,9 @@ "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": { @@ -1855,7 +1855,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1925,7 +1924,6 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2449,7 +2447,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3063,13 +3060,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT", - "peer": true + "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": { @@ -3520,7 +3516,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3576,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", @@ -3694,7 +3689,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6173,7 +6167,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6204,7 +6197,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -7091,7 +7083,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7259,7 +7250,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 4b9fdac7..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.7", + "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..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) => ( - - + ) @@ -269,7 +269,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -300,7 +300,7 @@ const ExpenseRealizationContent = ({ - + ) diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx index af8ceddc..0d7d959d 100644 --- a/src/components/pages/expense/ExpenseRequestContent.tsx +++ b/src/components/pages/expense/ExpenseRequestContent.tsx @@ -402,7 +402,10 @@ const ExpenseRequestContent = ({ @@ -529,7 +532,7 @@ const ExpenseRequestContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -550,7 +553,7 @@ const ExpenseRequestContent = ({ - + @@ -560,9 +563,7 @@ const ExpenseRequestContent = ({ - + diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index 3a50f233..bbcb6c4e 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -263,11 +263,11 @@ const ExpensesTable = () => { }, }, { - accessorKey: 'expense_date', + accessorKey: 'transaction_date', header: 'Tanggal Pengajuan', cell: (props) => - props.row.original.expense_date - ? formatDate(props.row.original.expense_date, 'DD MMM YYYY') + props.row.original.transaction_date + ? formatDate(props.row.original.transaction_date, 'DD MMM YYYY') : '-', }, { diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts index 863238b9..77db761c 100644 --- a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts +++ b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts @@ -27,7 +27,7 @@ type ExpenseRealizationFormSchemaType = { label: string; }; quantity?: number; - total_cost?: number; + price?: number; notes?: string; }[]; }[]; @@ -82,7 +82,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema { realization.cost_items.forEach((costItem) => { - const unitPrice = - parseFloat(String(costItem.total_cost)) / - parseFloat(String(costItem.quantity)); - const realizationItem = { expense_nonstock_id: costItem.nonstock?.value as number, qty: parseFloat(String(costItem.quantity)) as number, - unit_price: unitPrice, - total_price: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', }; @@ -177,7 +172,7 @@ const ExpenseRealizationForm = ({ { nonstock: undefined, quantity: undefined, - total_cost: undefined, + price: undefined, notes: '', }, ], diff --git a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx index 8b889c5b..017a733e 100644 --- a/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRealizationKandangDetailExpense.tsx @@ -48,7 +48,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC< }; const isExpenseRepeaterInputError = ( - column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', + column: 'nonstock' | 'quantity' | 'price' | 'notes', kandangExpenseIdx: number, expenseIdx: number ) => { @@ -112,7 +112,7 @@ const ExpenseRealizationKandangDetailExpense: React.FC< - + @@ -163,17 +163,17 @@ const ExpenseRealizationKandangDetailExpense: React.FC< - + {type !== 'detail' && } @@ -178,10 +178,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
    ({ 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 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..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/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx index 478cdadf..2b5b0a0a 100644 --- a/src/components/pages/expense/ExpenseRealizationContent.tsx +++ b/src/components/pages/expense/ExpenseRealizationContent.tsx @@ -207,7 +207,7 @@ const ExpenseRealizationContent = ({ let expenseGrandTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseGrandTotal += item.total_price) + (item) => (expenseGrandTotal += item.price) ); return ( @@ -238,7 +238,7 @@ const ExpenseRealizationContent = ({
    {pengajuanItem.nonstock.name} {pengajuanItem.qty}{formatCurrency(pengajuanItem.total_price)}{formatCurrency(pengajuanItem.price)} {pengajuanItem.note ?? '-'}
    {realisasiItem.nonstock.name} {realisasiItem.qty}{formatCurrency(realisasiItem.total_price)}{formatCurrency(realisasiItem.price)} {realisasiItem.note ?? '-'}
    Tanggal Transaksi : - {formatDate(initialValues?.expense_date, 'DD MMMM YYYY')} + {formatDate( + initialValues?.transaction_date, + 'DD MMMM YYYY' + )}
    Nonstock Total KuantitasTotal BiayaHarga Satuan Catatan
    {pengajuanItem.nonstock.name} {pengajuanItem.qty} - {formatCurrency(pengajuanItem.total_price)} - {formatCurrency(pengajuanItem.price)} {pengajuanItem.note ?? '-'}
    Nonstock Total KuantitasTotal BiayaHarga Satuan Catatan
    = documents: Yup.array().of(Yup.mixed().required()).optional(), - cost_per_kandangs: Yup.array() + expense_nonstocks: Yup.array() .of( Yup.object({ kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(), @@ -86,7 +86,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema = label: Yup.string().required(), }).required('Nonstock wajib diisi!'), quantity: Yup.number().required('Total kuantitas wajib diisi!'), - total_cost: Yup.number().required('Total biaya wajib diisi!'), + price: Yup.number().required('Harga satuan wajib diisi!'), notes: Yup.string(), }) ) @@ -128,8 +128,8 @@ export const getExpenseFormInitialValues = ( label: initialValues.location.name, } : undefined, - transaction_date: initialValues?.expense_date - ? formatDate(initialValues.expense_date, 'YYYY-MM-DD') + transaction_date: initialValues?.transaction_date + ? formatDate(initialValues.transaction_date, 'YYYY-MM-DD') : undefined, kandangs: initialValues?.kandangs.map((kandang) => ({ id: kandang.kandang_id, @@ -148,7 +148,7 @@ export const getExpenseFormInitialValues = ( })), deleted_documents: [], documents: [], - cost_per_kandangs: initialValues?.kandangs + expense_nonstocks: initialValues?.kandangs ? initialValues.kandangs.map((kandangExpense) => ({ kandang_id: kandangExpense.kandang_id, cost_items: kandangExpense.pengajuans @@ -158,7 +158,7 @@ export const getExpenseFormInitialValues = ( label: expenseItem.nonstock.name, }, quantity: expenseItem.qty, - total_cost: expenseItem.total_price, + price: expenseItem.price, notes: expenseItem.note, })) : [], diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx index e47f2f76..d52bde0d 100644 --- a/src/components/pages/expense/form/ExpenseRequestForm.tsx +++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx @@ -110,12 +110,12 @@ const ExpenseRequestForm = ({ transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], - cost_per_kandangs: values.cost_per_kandangs.map((costPerKandang) => ({ - kandang_id: costPerKandang.kandang_id, - cost_items: costPerKandang.cost_items.map((costItem) => ({ + expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({ + kandang_id: expenseNonstock.kandang_id, + cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, - total_cost: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), })), @@ -132,13 +132,13 @@ const ExpenseRequestForm = ({ transaction_date: values?.transaction_date as string, supplier_id: values.supplier?.value as number, documents: values.documents as File[], - cost_per_kandang: values.cost_per_kandangs.map( - (costPerKandang) => ({ - kandang_id: costPerKandang.kandang_id, - cost_items: costPerKandang.cost_items.map((costItem) => ({ + expense_nonstocks: values.expense_nonstocks.map( + (expenseNonstock) => ({ + kandang_id: expenseNonstock.kandang_id, + cost_items: expenseNonstock.cost_items.map((costItem) => ({ nonstock_id: costItem.nonstock?.value as number, quantity: parseFloat(String(costItem.quantity)) as number, - total_cost: parseFloat(String(costItem.total_cost)) as number, + price: parseFloat(String(costItem.price)) as number, notes: costItem.notes ?? '', })), }) @@ -179,53 +179,54 @@ const ExpenseRequestForm = ({ formik.setFieldValue('location', val); formik.setFieldValue('kandangs', []); - formik.setFieldValue('cost_per_kandangs', []); + formik.setFieldValue('expense_nonstocks', []); }; const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => { formik.setFieldTouched('kandangs', true); formik.setFieldValue('kandangs', kandangs); - const newCostPerKandangs = [...(formik.values.cost_per_kandangs ?? [])]; + const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])]; - // add new cost_per_kandangs + // add new expense_nonstocks kandangs.forEach((kandangItem) => { - const isKandangExistInCostPerKandangs = newCostPerKandangs.find( - (costPerKandangItem) => costPerKandangItem.kandang_id === kandangItem.id + const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find( + (expenseNonstockItem) => + expenseNonstockItem.kandang_id === kandangItem.id ); - if (isKandangExistInCostPerKandangs) return; + if (isKandangExistInExpenseNonstocks) return; - newCostPerKandangs.push({ + newExpenseNonstocks.push({ kandang_id: kandangItem.id, cost_items: [ { nonstock: undefined, quantity: undefined, - total_cost: undefined, + price: undefined, notes: '', }, ], }); }); - // prune cost_per_kandangs + // prune expense_nonstocks const kandangIds = new Set(kandangs.map((kandang) => kandang.id)); - const deletedCostPerKandangsIdx: number[] = []; + const deletedExpenseNonstocksIdx: number[] = []; - newCostPerKandangs.forEach((costPerKandang, idx) => { - const isCostPerKandangValid = kandangIds.has(costPerKandang.kandang_id); + newExpenseNonstocks.forEach((expenseNonstock, idx) => { + const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id); - if (!isCostPerKandangValid) { - deletedCostPerKandangsIdx.push(idx); + if (!isExpenseNonstockValid) { + deletedExpenseNonstocksIdx.push(idx); } }); - deletedCostPerKandangsIdx.forEach((deletedCostPerKandangIdx) => { - newCostPerKandangs.splice(deletedCostPerKandangIdx, 1); + deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => { + newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1); }); - formik.setFieldValue('cost_per_kandangs', newCostPerKandangs); + formik.setFieldValue('expense_nonstocks', newExpenseNonstocks); }; const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { diff --git a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx index 73e6c9b7..11f54585 100644 --- a/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx +++ b/src/components/pages/expense/form/ExpenseRequestKandangDetailExpense.tsx @@ -41,28 +41,28 @@ const ExpenseRequestKandangDetailExpense: React.FC< val: OptionType | OptionType[] | null ) => { formik.setFieldTouched( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, true ); formik.setFieldValue( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`, val ); }; const addExpenseItemHandler = (kandangExpenseIdx: number) => { const newExpensesValue = [ - ...formik.values.cost_per_kandangs[kandangExpenseIdx].cost_items, + ...formik.values.expense_nonstocks[kandangExpenseIdx].cost_items, { nonstock: undefined, - total_cost: undefined, + price: undefined, quantity: undefined, notes: '', }, ]; formik.setFieldValue( - `cost_per_kandangs[${kandangExpenseIdx}].cost_items`, + `expense_nonstocks[${kandangExpenseIdx}].cost_items`, newExpensesValue ); }; @@ -71,28 +71,28 @@ const ExpenseRequestKandangDetailExpense: React.FC< kandangExpenseIdx: number, expenseIdx: number ) => { - const path = `cost_per_kandangs[${kandangExpenseIdx}].cost_items`; + const path = `expense_nonstocks[${kandangExpenseIdx}].cost_items`; // trims values, errors, and touched at expenseIdx removeArrayItemAndSync(formik, path, expenseIdx); }; const isExpenseRepeaterInputError = ( - column: 'nonstock' | 'quantity' | 'total_cost' | 'notes', + column: 'nonstock' | 'quantity' | 'price' | 'notes', kandangExpenseIdx: number, expenseIdx: number ) => { return ( - formik.touched.cost_per_kandangs?.[kandangExpenseIdx]?.cost_items?.[ + formik.touched.expense_nonstocks?.[kandangExpenseIdx]?.cost_items?.[ expenseIdx ]?.[column] && Boolean( - formik.errors.cost_per_kandangs?.[kandangExpenseIdx] instanceof + formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof Object && - formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ + formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ expenseIdx ] instanceof Object && - formik.errors.cost_per_kandangs?.[kandangExpenseIdx].cost_items?.[ + formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[ expenseIdx ]?.[column] ) @@ -113,7 +113,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
    - {(formik.values.cost_per_kandangs.length === 0 || + {(formik.values.expense_nonstocks.length === 0 || !formik.values.supplier?.value) && (

    @@ -122,9 +122,9 @@ const ExpenseRequestKandangDetailExpense: React.FC<

    )} - {formik.values.cost_per_kandangs.length > 0 && + {formik.values.expense_nonstocks.length > 0 && formik.values.supplier?.value && - formik.values.cost_per_kandangs.map( + formik.values.expense_nonstocks.map( (kandangExpense, kandangExpenseIdx) => { const kandangName = formik.values.kandangs?.find( (kandang) => kandang.id === kandangExpense.kandang_id @@ -147,7 +147,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
    Nonstock Total KuantitasTotal BiayaHarga Satuan CatatanAksi
    { { label: 'Vendor', value: expense?.supplier.name }, { label: 'Tanggal Transaksi', - value: formatDate(expense?.expense_date, 'DD MMMM YYYY'), + value: formatDate(expense?.transaction_date, 'DD MMMM YYYY'), }, { label: 'Tanggal Realisasi', @@ -326,7 +326,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { let expenseRequestTotal = 0; kandangExpense.pengajuans?.forEach( - (item) => (expenseRequestTotal += item.total_price) + (item) => (expenseRequestTotal += item.price) ); return ( @@ -374,7 +374,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { - Total Biaya + Harga Satuan { ]} > - {formatCurrency(pengajuan.total_price)} + {formatCurrency(pengajuan.price)} { let expenseRealizationTotal = 0; kandangExpense.realisasi?.forEach( - (item) => (expenseRealizationTotal += item.total_price) + (item) => (expenseRealizationTotal += item.price) ); return ( @@ -532,7 +532,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => { - Total Biaya + Harga Satuan { ]} > - {formatCurrency(realisasi.total_price)} + {formatCurrency(realisasi.price)} 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..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/services/api/expense.ts b/src/services/api/expense.ts index 337730e6..44a855f4 100644 --- a/src/services/api/expense.ts +++ b/src/services/api/expense.ts @@ -492,8 +492,8 @@ export class ExpenseApiService extends BaseApiService< }); formData.append( - 'cost_per_kandangs', - JSON.stringify(payload.cost_per_kandangs) + 'expense_nonstocks', + JSON.stringify(payload.expense_nonstocks) ); return formData; @@ -514,8 +514,8 @@ export class ExpenseApiService extends BaseApiService< }); formData.append( - 'cost_per_kandang', - JSON.stringify(payload.cost_per_kandang) + 'expense_nonstocks', + JSON.stringify(payload.expense_nonstocks) ); return formData; 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/api/expense.d.ts b/src/types/api/expense.d.ts index 71863503..a62066ba 100644 --- a/src/types/api/expense.d.ts +++ b/src/types/api/expense.d.ts @@ -18,7 +18,7 @@ export type BaseExpense = { id: number; path: string; }[]; - expense_date: string; + transaction_date: string; realization_date?: string; grand_total: number; location: BaseLocation; @@ -29,28 +29,23 @@ export type BaseExpense = { name: string; pengajuans?: { id: number; + expense_id: number; + kandang_id: number; + nonstock_id: number; qty: number; - unit_price: number; - total_price: number; + price: number; note?: string; nonstock: Pick; - project_flock_kandang: { - id: number; - kandang_id: number; - }; + created_at: string; }[]; realisasi?: { id: number; + expense_nonstock_id: number; qty: number; - unit_price: number; - total_price: number; - date: string; + price: number; note?: string; nonstock: Pick; - project_flock_kandang: { - id: number; - kandang_id: number; - }; + created_at: string; }[]; }[]; total_pengajuan: number; @@ -65,12 +60,12 @@ export type CreateExpensePayload = { transaction_date: string; supplier_id: number; documents: File[]; - cost_per_kandangs: { + expense_nonstocks: { kandang_id: number; cost_items: { nonstock_id: number; quantity: number; - total_cost: number; + price: number; notes: string; }[]; }[]; @@ -81,12 +76,12 @@ export type UpdateExpensePayload = { transaction_date: string; supplier_id: number; documents: File[]; - cost_per_kandang: { + expense_nonstocks: { kandang_id: number; cost_items: { nonstock_id: number; quantity: number; - total_cost: number; + price: number; notes: string; }[]; }[]; @@ -98,8 +93,7 @@ export type CreateExpenseRealizationPayload = { realizations: { expense_nonstock_id: number; qty: number; - unit_price: number; - total_price: number; + price: number; notes: string; }[]; }; 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';