diff --git a/package-lock.json b/package-lock.json index 535bb986..f960d1c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { @@ -3067,9 +3067,9 @@ "peer": true }, "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": { @@ -3576,13 +3576,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", diff --git a/package.json b/package.json index 85485ee3..e1f92aaf 100644 --- a/package.json +++ b/package.json @@ -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/page.tsx b/src/app/_closing/detail/page.tsx deleted file mode 100644 index 73cce850..00000000 --- a/src/app/_closing/detail/page.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { useRouter, useSearchParams } from 'next/navigation'; -import useSWR from 'swr'; -import SalesReportTable from '@/components/pages/closing/sale/SalesReportTable'; -import { ClosingApi } from '@/services/api/closing'; -import { isResponseSuccess } from '@/lib/api-helper'; -import SapronakCalculationTable from '@/components/pages/closing/sapronak/SapronakCalculationTable'; -import Tabs from '@/components/Tabs'; -import { useState } from 'react'; - -const ClosingDetailPage = () => { - const router = useRouter(); - const searchParams = useSearchParams(); - - const [activeTab, setActiveTab] = useState('perhitungan_sapronak'); - - const closingId = searchParams.get('closingId'); - - const { data: closing, isLoading: isLoadingClosing } = useSWR( - closingId, - (id: string) => { - const numericId = parseInt(id, 10); - if (isNaN(numericId) || numericId <= 0) { - throw new Error('Invalid closing ID'); - } - return ClosingApi.getPenjualan(numericId); - } - ); - - const { data: sapronakCalculation, isLoading: isLoadingSapronakCalculation } = - useSWR(`/closing/${closingId}/perhitungan_sapronak`, () => { - const numericId = parseInt(closingId ?? '', 10); - if (isNaN(numericId) || numericId <= 0) { - throw new Error('Invalid closing ID'); - } - const res = ClosingApi.getPerhitunganSapronak(numericId); - console.log(res); - return res; - }); - - if (!closingId) { - router.back(); - - return ( -
- -
- ); - } - - return ( -
- - ), - }, - { - id: 'penjualan', - label: 'Penjualan', - content: isResponseSuccess(closing) && ( - - ), - }, - ]} - /> -
- ); -}; - -export default ClosingDetailPage; diff --git a/src/app/_closing/detail/layout.tsx b/src/app/closing/detail/layout.tsx similarity index 100% rename from src/app/_closing/detail/layout.tsx rename to src/app/closing/detail/layout.tsx 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 5c76f44e..f1466744 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -41,6 +41,7 @@ export interface TableProps { data: TData[]; columns: ColumnDef[]; pageSize?: number; + onPageSizeChange?: (pageSize: number) => void; totalItems?: number; page?: number; onPageChange?: (page: number) => void; @@ -58,6 +59,8 @@ export interface TableProps { renderFooter?: boolean; footerContent?: ReactNode; footerData?: TData[]; + withCheckbox?: boolean; + rowOptions?: number[]; } const DUMMY_SKELETON_DATA = [{}, {}, {}, {}, {}]; @@ -70,31 +73,36 @@ 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: '', + + tableFooterClassName: '', + footerRowClassName: '', + footerColumnClassName: '', +}; + const Table = ({ data = [], columns = [], pageSize = 10, + onPageSizeChange, totalItems, page, onPageChange, isLoading = false, fuzzySearchValue, onFuzzySearchValueChange, - className = { - containerClassName: '', - tableWrapperClassName: '', - tableClassName: '', - tableHeaderClassName: '', - headerRowClassName: '', - headerColumnClassName: '', - tableBodyClassName: '', - bodyRowClassName: '', - bodyColumnClassName: '', - tableFooterClassName: '', - footerRowClassName: '', - footerColumnClassName: '', - paginationClassName: '', - }, + className = TABLE_DEFAULT_STYLING, emptyContent = emptyContentDefaultValue, sorting, setSorting, @@ -105,12 +113,19 @@ const Table = ({ renderFooter = false, footerContent, footerData = [], + 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, @@ -211,12 +226,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 )} >
    @@ -236,12 +257,13 @@ const Table = ({ )} {header.column.getCanSort() && ( -
    +
    ({ )} /> ({ ))}
    + {!isLoading && flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -310,7 +339,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..86f39d18 --- /dev/null +++ b/src/components/pages/closing/ClosingDetail.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useMemo, useState } from 'react'; + +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Tabs from '@/components/Tabs'; +import ClosingGeneralInformationTable from '@/components/pages/closing/ClosingGeneralInformationTable'; + +import { ClosingGeneralInformation } from '@/types/api/closing'; +import ClosingSapronakTabContent from './ClosingSapronakTabContent'; +import ClosingSapronakCalculationTabContent from '@/components/pages/closing/ClosingSapronakCalculationTabContent'; + +interface ClosingDetailProps { + id: number; + initialValue?: ClosingGeneralInformation; +} + +const ClosingDetail: React.FC = ({ id, initialValue }) => { + const [activeTab, setActiveTab] = useState('sapronak'); + + const closingDetailTabs = useMemo(() => { + const validTabs = [ + { + id: 'sapronak', + label: 'Sapronak', + content: , + }, + { + id: 'perhitunganSapronak', + label: 'Perhitungan Sapronak', + content: , + }, + { + id: 'penjualan', + label: 'Penjualan', + content: 'Penjualan', + }, + { + id: 'overhead', + label: 'Overhead', + content: 'Overhead', + }, + { + id: 'hppEkspedisi', + label: 'HPP Ekspedisi', + content: 'HPP Ekspedisi', + }, + { + id: 'dataProduksi', + label: 'Data Produksi', + content: 'Data Produksi', + }, + { + id: 'keuangan', + label: 'Keuangan', + content: 'Keuangan', + }, + ]; + + return validTabs; + }, [initialValue]); + + return ( + <> +
    +
    + + +

    Detail Closing

    +
    + + + + +
    + + ); +}; + +export default ClosingDetail; diff --git a/src/components/pages/closing/ClosingGeneralInformationTable.tsx b/src/components/pages/closing/ClosingGeneralInformationTable.tsx new file mode 100644 index 00000000..af21497a --- /dev/null +++ b/src/components/pages/closing/ClosingGeneralInformationTable.tsx @@ -0,0 +1,100 @@ +import { ClosingGeneralInformation } from '@/types/api/closing'; + +interface ClosingGeneralInformationProps { + initialValue?: ClosingGeneralInformation; +} + +const ClosingGeneralInformationTable = ({ + initialValue, +}: ClosingGeneralInformationProps) => { + return ( +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Lokasi:{initialValue?.location_name}
    Periode:{initialValue?.period}
    Kategori:{initialValue?.project_category}
    Populasi:{initialValue?.population} Ekor
    Jenis Project:{initialValue?.project_type}
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    Kandang Aktif:{initialValue?.active_house_count} Kandang
    Status Pembayaran Penjualan:{initialValue?.sales_payment_status}
    Status Project:{initialValue?.project_status}
    Status Closing:{initialValue?.closing_status}
    +
    +
    +
    +
    + ); +}; + +export default ClosingGeneralInformationTable; diff --git a/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx new file mode 100644 index 00000000..206beb3d --- /dev/null +++ b/src/components/pages/closing/ClosingIncomingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingIncomingSapronak } from '@/types/api/closing'; + +interface ClosingIncomingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingIncomingSapronaksTable = ({ + projectFlockId, +}: ClosingIncomingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/incoming${getTableFilterQueryString()}`, + ClosingApi.getAllIncomingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const incomingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks.data.length > 0 + : false + ); + } + }, [incomingSapronaks, isResponseSuccess]); + + return ( + + +
    Sapronak Masuk
    + + +
    + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.data + : [] + } + columns={incomingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(incomingSapronaks) + ? incomingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingIncomingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(incomingSapronaks) && + incomingSapronaks?.data?.length === 0, + }), + }} + /> +
    + + + ); +}; + +export default ClosingIncomingSapronaksTable; diff --git a/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx new file mode 100644 index 00000000..9047e79a --- /dev/null +++ b/src/components/pages/closing/ClosingOutgoingSapronaksTable.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Card from '@/components/Card'; +import Collapse from '@/components/Collapse'; + +import { cn, formatDate, formatNumber } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ClosingApi } from '@/services/api/closing'; +import { ClosingOutgoingSapronak } from '@/types/api/closing'; + +interface ClosingOutgoingSapronaksTableProps { + projectFlockId: number; +} + +const ClosingOutgoingSapronaksTable = ({ + projectFlockId, +}: ClosingOutgoingSapronaksTableProps) => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + }, + }); + + const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } = + useSWR( + `${ClosingApi.basePath}/${projectFlockId}/sapronak/outgoing${getTableFilterQueryString()}`, + ClosingApi.getAllOutgoingSapronakFetcher, + { + keepPreviousData: true, + } + ); + + const [open, setOpen] = useState(true); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const outgoingSapronaksColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'date', + header: 'Tanggal', + cell: (props) => formatDate(props.row.original.date, 'DD MMM YYYY'), + }, + { + accessorKey: 'reference_number', + header: 'No. Referensi', + }, + { + accessorKey: 'transaction_type', + header: 'Jenis Transaksi', + }, + { + accessorKey: 'product_name', + header: 'Produk', + }, + { + accessorKey: 'product_category', + header: 'Kategori Produk', + }, + { + accessorKey: 'source_warehouse', + header: 'Gudang Asal', + }, + { + accessorKey: 'destination_warehouse', + header: 'Gudang Tujuan', + }, + { + accessorKey: 'quantity', + header: 'Kuantitas', + cell: (props) => + `${formatNumber(props.row.original.quantity)} ${props.row.original.unit}`, + }, + { + accessorKey: 'notes', + header: 'Keterangan', + }, + ]; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + useEffect(() => { + if (!open) { + setOpen( + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks.data.length > 0 + : false + ); + } + }, [outgoingSapronaks, isResponseSuccess]); + + return ( + + +
    Sapronak Keluar
    + + + + } + className='w-full!' + titleClassName='w-full p-0!' + > +
    +
    +
    + +
    +
    + + + data={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.data + : [] + } + columns={outgoingSapronaksColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(outgoingSapronaks) + ? outgoingSapronaks?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoadingOutgoingSapronaks} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(outgoingSapronaks) && + outgoingSapronaks?.data?.length === 0, + }), + }} + /> +
    +
    +
    + ); +}; + +export default ClosingOutgoingSapronaksTable; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx new file mode 100644 index 00000000..15e43bbc --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakCalculationTabContent.tsx @@ -0,0 +1,25 @@ +'use client'; + +import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; +import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; +import ClosingSapronakCalculationTable from '@/components/pages/closing/ClosingSapronakCalculationTable'; + +interface ClosingSapronakCalculationTabContentProps { + projectFlockId?: number; +} + +const ClosingSapronakCalculationTabContent = ({ + projectFlockId, +}: ClosingSapronakCalculationTabContentProps) => { + return ( +
    + {projectFlockId && ( + <> + + + )} +
    + ); +}; + +export default ClosingSapronakCalculationTabContent; diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx new file mode 100644 index 00000000..cd2b8c68 --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx @@ -0,0 +1,330 @@ +'use client'; + +import Card from '@/components/Card'; + +import Table from '@/components/Table'; +import { cn, formatCurrency, formatNumber } from '@/lib/helper'; +import { + ClosingSapronakCalculation, + RowSapronakCalculation, + TotalSapronakCalculation, +} from '@/types/api/closing'; +import { ColumnDef } from '@tanstack/react-table'; +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { ClosingApi } from '@/services/api/closing'; +import { isResponseSuccess } from '@/lib/api-helper'; + +interface ClosingSapronakCalculationTableProps { + type?: 'detail'; + projectFlockId: number; +} + +interface FooterSapronakCalculationRow extends RowSapronakCalculation { + _isFooter: true; +} + +const ClosingSapronakCalculationTable = ({ + type, + projectFlockId, +}: ClosingSapronakCalculationTableProps) => { + const { data: sapronakCalculation, isLoading } = useSWR( + `/closing/sapronak-calculation/${projectFlockId}`, + () => ClosingApi.getPerhitunganSapronak(projectFlockId) + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + header: 'Tanggal', + accessorKey: 'tanggal', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'No. Referensi', + accessorKey: 'no_referensi', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + const value = props.getValue() as string; + if (isFooter) { + return ( +
    + {value} +
    + ); + } + return value || '-'; + }, + }, + { + header: 'QTY Masuk', + accessorKey: 'qty_masuk', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'QTY Keluar', + accessorKey: 'qty_keluar', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'QTY Pakai', + accessorKey: 'qty_pakai', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatNumber(value)} +
    + ); + }, + }, + { + header: 'Uraian', + accessorKey: 'uraian', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Kategori Produk', + accessorKey: 'kategori_produk', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + { + header: 'Harga Beli/Qty (Rp)', + accessorKey: 'harga_beli_per_qty', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatCurrency(value)} +
    + ); + }, + }, + { + header: 'Total Harga (Rp)', + accessorKey: 'total_harga', + cell: (props) => { + const value = props.getValue() as number; + const isFooter = '_isFooter' in props.row.original; + return ( +
    + {formatCurrency(value)} +
    + ); + }, + }, + { + header: 'Keterangan', + accessorKey: 'keterangan', + cell: (props) => { + const isFooter = '_isFooter' in props.row.original; + if (isFooter) return null; + const value = props.getValue() as string; + return value || '-'; + }, + }, + ], + [] + ); + + const createFooterRow = ( + total?: TotalSapronakCalculation + ): FooterSapronakCalculationRow[] => { + if (!total) return []; + return [ + { + id: -999, + tanggal: '', + no_referensi: total.label, + qty_masuk: total.qty_masuk, + qty_keluar: total.qty_keluar, + qty_pakai: total.qty_pakai, + uraian: '', + kategori_produk: '', + harga_beli_per_qty: total.harga_beli_per_qty, + total_harga: total.total_harga, + keterangan: '', + _isFooter: true, + }, + ]; + }; + + const docBroilerFooter = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createFooterRow(sapronakCalculation.data?.doc_broiler.total) + : [], + [sapronakCalculation] + ); + + const ovkFooter = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createFooterRow(sapronakCalculation.data?.ovk.total) + : [], + [sapronakCalculation] + ); + + const pakanFooter = useMemo( + () => + isResponseSuccess(sapronakCalculation) + ? createFooterRow(sapronakCalculation.data?.pakan.total) + : [], + [sapronakCalculation] + ); + + return ( +
    + {isResponseSuccess(sapronakCalculation) && ( + <> + + + data={sapronakCalculation.data?.doc_broiler.rows ?? []} + columns={columns} + footerData={docBroilerFooter} + renderFooter={ + (sapronakCalculation.data?.doc_broiler.rows.length ?? 0) > 0 && + !!sapronakCalculation.data?.doc_broiler.total + } + className={{ + containerClassName: cn({ + 'mb-20': + sapronakCalculation.data?.doc_broiler.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={sapronakCalculation.data?.ovk.rows ?? []} + columns={columns} + footerData={ovkFooter} + renderFooter={ + (sapronakCalculation.data?.ovk.rows.length ?? 0) > 0 && + !!sapronakCalculation.data?.ovk.total + } + className={{ + containerClassName: cn({ + 'mb-20': sapronakCalculation.data?.ovk.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + + + data={sapronakCalculation.data?.pakan.rows ?? []} + columns={columns} + footerData={pakanFooter} + renderFooter={ + (sapronakCalculation.data?.pakan.rows.length ?? 0) > 0 && + !!sapronakCalculation.data?.pakan.total + } + className={{ + containerClassName: cn({ + 'mb-20': sapronakCalculation.data?.pakan.rows.length === 0, + }), + tableWrapperClassName: 'overflow-x-auto min-h-full!', + tableClassName: 'font-inter w-full table-auto min-h-full!', + headerRowClassName: 'border-b border-b-gray-200', + headerColumnClassName: + 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', + bodyRowClassName: 'border-b border-b-gray-200', + bodyColumnClassName: + 'px-6 py-3 last:flex last:flex-row last:justify-end', + tableFooterClassName: + 'bg-gray-100 font-semibold border border-gray-200', + footerRowClassName: 'border-t-2 border-gray-300', + footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', + }} + /> + + + )} +
    + ); +}; + +export default ClosingSapronakCalculationTable; diff --git a/src/components/pages/closing/ClosingSapronakTabContent.tsx b/src/components/pages/closing/ClosingSapronakTabContent.tsx new file mode 100644 index 00000000..41c7aa05 --- /dev/null +++ b/src/components/pages/closing/ClosingSapronakTabContent.tsx @@ -0,0 +1,26 @@ +'use client'; + +import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable'; +import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable'; + +interface ClosingSapronakTableProps { + projectFlockId?: number; +} + +const ClosingSapronakTabContent = ({ + projectFlockId, +}: ClosingSapronakTableProps) => { + return ( +
    + {projectFlockId && ( + <> + + + + + )} +
    + ); +}; + +export default ClosingSapronakTabContent; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx new file mode 100644 index 00000000..91e78c8c --- /dev/null +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -0,0 +1,299 @@ +'use client'; + +import { ChangeEventHandler, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { cn, formatCurrency, formatDate } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { LocationApi } from '@/services/api/master-data'; +import { Location } from '@/types/api/master-data/location'; +import { ClosingApi } from '@/services/api/closing'; +import { Closing } from '@/types/api/closing'; + +const PROJECT_STATUS_OPTIONS = [ + { + value: 1, + label: 'Pengajuan', + }, + { + value: 2, + label: 'Aktif', + }, +]; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; +}) => { + return ( + + {/* TODO: apply RBAC */} +
    + +
    +
    + ); +}; + +const ClosingsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + transactionDate: '', + realizationDate: '', + locationId: '', + projectStatus: '', + userId: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + transactionDate: 'transaction_date', + realizationDate: 'realization_date', + locationId: 'location_id', + projectStatus: 'project_status', + userId: 'user_id', + }, + }); + + const { data: closings, isLoading: isLoadingClosings } = useSWR( + `${ClosingApi.basePath}${getTableFilterQueryString()}`, + ClosingApi.getAllFetcher + ); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + + const closingsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'location_name', + header: 'Lokasi', + }, + { + accessorKey: 'project_category', + header: 'Kategori', + }, + { + accessorKey: 'period', + header: 'Periode', + }, + { + accessorKey: 'closing_date', + header: 'Periode', + cell: (props) => + formatDate(props.row.original.closing_date, 'DD MMM YYYY'), + }, + { + accessorKey: 'shed_label', + header: 'Jumlah Kandang', + }, + { + accessorKey: 'sales_paid_amount', + header: 'Jumlah Sudah Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_paid_amount)} + + ), + }, + { + accessorKey: 'sales_remaining_amount', + header: 'Jumlah Sisa Bayar', + cell: (props) => ( + + {formatCurrency(props.row.original.sales_remaining_amount)} + + ), + }, + { + accessorKey: 'sales_payment_status', + header: 'Status Pembayaran', + }, + { + accessorKey: 'project_status', + header: 'Status', + }, + { + header: 'Aksi', + cell: (props) => { + const currentPageSize = props.table.getPaginationRowModel().rows.length; + const currentPageRows = props.table.getPaginationRowModel().flatRows; + const currentRowRelativeIndex = + currentPageRows.findIndex((r) => r.id === props.row.id) + 1; + + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 3; + + return ( + <> + {currentPageSize > 3 && ( + + + + )} + + {currentPageSize <= 3 && ( + + + + )} + + ); + }, + }, + ]; + + const { + setInputValue: setLocationInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocationOptions, + } = useSelect(LocationApi.basePath, 'id', 'name'); + + const [selectedLocation, setSelectedLocation] = useState( + null + ); + + const locationChangeHandler = (val: OptionType | OptionType[] | null) => { + setSelectedLocation(val as OptionType); + updateFilter( + 'locationId', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const [selectedProjectStatus, setSelectedProjectStatus] = + useState(null); + + const projectStatusChangeHandler = ( + val: OptionType | OptionType[] | null + ) => { + setSelectedProjectStatus(val as OptionType); + updateFilter( + 'projectStatus', + val ? ((val as OptionType).value as string) : '' + ); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + // track sorting + useEffect(() => { + const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); + + if (!isNameSorted) { + updateFilter('nameSort', ''); + } else { + updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); + } + }, [sorting, updateFilter]); + + return ( + <> +
    +
    +
    +
    + +
    + +
    + + + +
    +
    +
    + + + data={isResponseSuccess(closings) ? closings?.data : []} + columns={closingsColumns} + pageSize={tableFilterState.pageSize} + onPageSizeChange={setPageSize} + rowOptions={[10, 20, 50, 100]} + page={isResponseSuccess(closings) ? closings?.meta?.page : 0} + totalItems={ + isResponseSuccess(closings) ? closings?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoadingClosings} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn({ + 'w-full mb-20': + isResponseSuccess(closings) && closings?.data?.length === 0, + }), + }} + /> +
    + + ); +}; + +export default ClosingsTable; diff --git a/src/components/pages/closing/sale/SalesReportTable.tsx b/src/components/pages/closing/sale/SalesReportTable.tsx deleted file mode 100644 index f0810f15..00000000 --- a/src/components/pages/closing/sale/SalesReportTable.tsx +++ /dev/null @@ -1,374 +0,0 @@ -'use client'; - -import React, { useMemo } from 'react'; -import { ColumnDef } from '@tanstack/react-table'; -import Table from '@/components/Table'; -import Card from '@/components/Card'; -import Badge from '@/components/Badge'; -import { formatCurrency, formatNumber, formatDate } from '@/lib/helper'; -import { BaseClosingSales, BaseSales } from '@/types/api/closing/closing'; -import { Product } from '@/types/api/master-data/product'; -import { Customer } from '@/types/api/master-data/customer'; -import { Kandang } from '@/types/api/master-data/kandang'; - -interface SalesReportTableProps { - type?: 'detail'; - initialValues?: BaseClosingSales; -} - -interface FooterSalesRow extends BaseSales { - _isFooter: true; -} - -const SalesReportTable = ({ - type = 'detail', - initialValues, -}: SalesReportTableProps) => { - const salesData: BaseSales[] = useMemo(() => { - return initialValues?.sales || []; - }, [initialValues]); - - const totals = useMemo(() => { - if (salesData.length === 0) { - return { - totalQuantity: 0, - totalWeight: 0, - avgWeight: 0, - avgPricePartner: 0, - totalPartner: 0, - }; - } - - const totalQuantity = salesData.reduce( - (sum, item) => sum + (item.qty || 0), - 0 - ); - const totalWeight = salesData.reduce( - (sum, item) => sum + (item.weight || 0), - 0 - ); - const avgWeight = totalQuantity > 0 ? totalWeight / totalQuantity : 0; - - const validPriceItems = salesData.filter( - (item) => item.price != null && item.price > 0 - ); - const avgPricePartner = - validPriceItems.length > 0 - ? validPriceItems.reduce((sum, item) => sum + item.price, 0) / - validPriceItems.length - : 0; - - const totalPartner = salesData.reduce( - (sum, item) => sum + (item.total_price || 0), - 0 - ); - - return { - totalQuantity, - totalWeight, - avgWeight, - avgPricePartner, - totalPartner, - }; - }, [salesData]); - - const footerData = useMemo((): FooterSalesRow[] => { - if (salesData.length === 0) return []; - - const footerRow: FooterSalesRow = { - id: -999, - realization_date: 'Total Penjualan', - age: 0, - do_number: '', - product: {} as Product, - customer: {} as Customer, - qty: totals.totalQuantity, - weight: totals.totalWeight, - avg_weight: totals.avgWeight, - price: totals.avgPricePartner, - total_price: totals.totalPartner, - kandang: {} as Kandang, - payment_status: '', - _isFooter: true, - }; - - return [footerRow]; - }, [salesData, totals]); - - const salesColumns: ColumnDef[] = useMemo( - () => [ - { - id: 'realization_date', - accessorKey: 'realization_date', - header: 'Tanggal Realisasi', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) { - return ( -
    - {props.row.original.realization_date} -
    - ); - } - const date = props.row.original.realization_date; - return date ? formatDate(date, 'DD MMM YYYY') : '-'; - }, - }, - { - id: 'age', - accessorKey: 'age', - header: 'Umur', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, - }, - { - id: 'do_number', - accessorKey: 'do_number', - header: 'No. DO', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - return isFooter ? null : props.getValue() || '-'; - }, - }, - { - id: 'product', - accessorKey: 'product', - header: 'Produk', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const product = props.getValue() as Product; - return product?.name || '-'; - }, - }, - { - id: 'customer', - accessorKey: 'customer', - header: 'Customer', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const customer = props.getValue() as Customer; - return customer?.name || '-'; - }, - }, - { - id: 'qty', - accessorKey: 'qty', - header: 'Kuantitas', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - id: 'weight', - accessorKey: 'weight', - header: 'Kg', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - id: 'avg_weight', - accessorKey: 'avg_weight', - header: 'AVG (Kg)', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - id: 'price_partner', - accessorKey: 'price', - header: 'Harga Mitra (Rp)', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - id: 'total_mitra', - accessorKey: 'total_price', - header: 'Total Mitra (Rp)', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - id: 'price_act', - accessorKey: 'price', - header: 'Harga Act (Rp)', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - id: 'total_act', - accessorKey: 'total_price', - header: 'Total Act (Rp)', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - id: 'kandang', - accessorKey: 'kandang', - header: 'Kandang', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const kandang = props.getValue() as Kandang; - return kandang?.name || '-'; - }, - }, - { - id: 'payment_status', - accessorKey: 'payment_status', - header: 'Status Pembayaran', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - - const status = props.getValue() as string; - const getStatusColor = (status: string) => { - if (!status) return 'neutral'; - switch (status.toLowerCase()) { - case 'paid': - return 'success'; - case 'tempo': - return 'warning'; - default: - return 'neutral'; - } - }; - - return ( - - {status || '-'} - - ); - }, - }, - ], - [] - ); - - return ( - <> -
    -
    -

    Penjualan

    - - 0} - className={{ - tableWrapperClassName: 'overflow-x-auto', - tableClassName: 'w-full table-auto text-sm', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-4 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end whitespace-nowrap', - bodyRowClassName: - 'hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200', - bodyColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: - 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', - }} - /> - - - - - ); -}; - -export default SalesReportTable; diff --git a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx b/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx deleted file mode 100644 index 679ec5e7..00000000 --- a/src/components/pages/closing/sapronak/SapronakCalculationTable.tsx +++ /dev/null @@ -1,310 +0,0 @@ -'use client'; - -import Card from '@/components/Card'; - -import Table from '@/components/Table'; -import { cn, formatCurrency, formatNumber } from '@/lib/helper'; -import { - SapronakCalculation, - RowSapronakCalculation, - TotalSapronakCalculation, -} from '@/types/api/closing/closing'; -import { ColumnDef } from '@tanstack/react-table'; -import { useMemo } from 'react'; - -interface SapronakCalculationTableProps { - type?: 'detail'; - initialValues?: SapronakCalculation; -} - -interface FooterSapronakCalculationRow extends RowSapronakCalculation { - _isFooter: true; -} - -const SapronakCalculationTable = ({ - type, - initialValues, -}: SapronakCalculationTableProps) => { - const columns: ColumnDef[] = useMemo( - () => [ - { - header: 'Tanggal', - accessorKey: 'tanggal', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'No. Referensi', - accessorKey: 'no_referensi', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - const value = props.getValue() as string; - if (isFooter) { - return ( -
    - {value} -
    - ); - } - return value || '-'; - }, - }, - { - header: 'QTY Masuk', - accessorKey: 'qty_masuk', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'QTY Keluar', - accessorKey: 'qty_keluar', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'QTY Pakai', - accessorKey: 'qty_pakai', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatNumber(value)} -
    - ); - }, - }, - { - header: 'Uraian', - accessorKey: 'uraian', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Kategori Produk', - accessorKey: 'kategori_produk', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - { - header: 'Harga Beli/Qty (Rp)', - accessorKey: 'harga_beli_per_qty', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - header: 'Total Harga (Rp)', - accessorKey: 'total_harga', - cell: (props) => { - const value = props.getValue() as number; - const isFooter = '_isFooter' in props.row.original; - return ( -
    - {formatCurrency(value)} -
    - ); - }, - }, - { - header: 'Keterangan', - accessorKey: 'keterangan', - cell: (props) => { - const isFooter = '_isFooter' in props.row.original; - if (isFooter) return null; - const value = props.getValue() as string; - return value || '-'; - }, - }, - ], - [] - ); - - const createFooterRow = ( - total?: TotalSapronakCalculation - ): FooterSapronakCalculationRow[] => { - if (!total) return []; - return [ - { - id: -999, - tanggal: '', - no_referensi: total.label, - qty_masuk: total.qty_masuk, - qty_keluar: total.qty_keluar, - qty_pakai: total.qty_pakai, - uraian: '', - kategori_produk: '', - harga_beli_per_qty: total.harga_beli_per_qty, - total_harga: total.total_harga, - keterangan: '', - _isFooter: true, - }, - ]; - }; - - const docBroilerFooter = useMemo( - () => createFooterRow(initialValues?.doc_broiler.total), - [initialValues?.doc_broiler.total] - ); - - const ovkFooter = useMemo( - () => createFooterRow(initialValues?.ovk.total), - [initialValues?.ovk.total] - ); - - const pakanFooter = useMemo( - () => createFooterRow(initialValues?.pakan.total), - [initialValues?.pakan.total] - ); - - return ( -
    - <> - - - data={initialValues?.doc_broiler.rows ?? []} - columns={columns} - footerData={docBroilerFooter} - renderFooter={ - (initialValues?.doc_broiler.rows.length ?? 0) > 0 && - !!initialValues?.doc_broiler.total - } - className={{ - containerClassName: cn({ - 'mb-20': initialValues?.doc_broiler.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', - }} - /> - - - - - data={initialValues?.ovk.rows ?? []} - columns={columns} - footerData={ovkFooter} - renderFooter={ - (initialValues?.ovk.rows.length ?? 0) > 0 && - !!initialValues?.ovk.total - } - className={{ - containerClassName: cn({ - 'mb-20': initialValues?.ovk.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', - }} - /> - - - - - data={initialValues?.pakan.rows ?? []} - columns={columns} - footerData={pakanFooter} - renderFooter={ - (initialValues?.pakan.rows.length ?? 0) > 0 && - !!initialValues?.pakan.total - } - className={{ - containerClassName: cn({ - 'mb-20': initialValues?.pakan.rows.length === 0, - }), - tableWrapperClassName: 'overflow-x-auto min-h-full!', - tableClassName: 'font-inter w-full table-auto min-h-full!', - headerRowClassName: 'border-b border-b-gray-200', - headerColumnClassName: - 'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end', - bodyRowClassName: 'border-b border-b-gray-200', - bodyColumnClassName: - 'px-6 py-3 last:flex last:flex-row last:justify-end', - tableFooterClassName: - 'bg-gray-100 font-semibold border border-gray-200', - footerRowClassName: 'border-t-2 border-gray-300', - footerColumnClassName: 'px-6 py-3 text-xs text-gray-900', - }} - /> - - -
    - ); -}; - -export default SapronakCalculationTable; 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/dummy/closing.dummy.ts b/src/dummy/closing.dummy.ts deleted file mode 100644 index 0b1b3f5c..00000000 --- a/src/dummy/closing.dummy.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { SapronakCalculation } from '@/types/api/closing/closing'; - -// Dummy data -const DUMMY_SAPRONAK_CALCULATION: SapronakCalculation = { - doc_broiler: { - rows: [ - { - id: 1, - tanggal: '11-Sep-2025', - no_referensi: 'PO-PULLET-388', - qty_masuk: 32800, - qty_keluar: 0, - qty_pakai: 32800, - uraian: 'PULLET LOHMANN (16 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 60136, - total_harga: 1972556800, - keterangan: '-', - }, - { - id: 2, - tanggal: '24-Sep-2025', - no_referensi: 'PO-PULLET-410', - qty_masuk: 14758, - qty_keluar: 0, - qty_pakai: 14758, - uraian: 'PULLET HY-LINE (17 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 65421, - total_harga: 965908998, - keterangan: '-', - }, - { - id: 3, - tanggal: '29-Sep-2025', - no_referensi: 'PO-PULLET-196', - qty_masuk: 35439, - qty_keluar: 0, - qty_pakai: 35439, - uraian: 'PULLET ISA BROWN (15 MINGGU)', - kategori_produk: 'PULLET LAYER', - harga_beli_per_qty: 55909, - total_harga: 1981297351, - keterangan: '-', - }, - ], - total: { - label: 'TOTAL PULLET', - qty_masuk: 82997, - qty_keluar: 0, - qty_pakai: 82997, - harga_beli_per_qty: 59271.85, - total_harga: 4919763149, - }, - }, - ovk: { - rows: [ - { - id: 1, - tanggal: '28-Sep-2025', - no_referensi: 'PO-OVK-276', - qty_masuk: 52, - qty_keluar: 0, - qty_pakai: 52, - uraian: 'ND-IB VACCINE', - kategori_produk: 'OVK VAKSIN', - harga_beli_per_qty: 204652, - total_harga: 10641904, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 2, - tanggal: '26-Sep-2025', - no_referensi: 'PO-OVK-811', - qty_masuk: 43, - qty_keluar: 0, - qty_pakai: 43, - uraian: 'GUMBORO VACCINE', - kategori_produk: 'OVK VAKSIN', - harga_beli_per_qty: 298379, - total_harga: 12830297, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 3, - tanggal: '28-Sep-2025', - no_referensi: 'PO-OVK-879', - qty_masuk: 21, - qty_keluar: 0, - qty_pakai: 21, - uraian: 'AMOXITIN SOLUBLE', - kategori_produk: 'OVK OBAT', - harga_beli_per_qty: 145952, - total_harga: 3064992, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 4, - tanggal: '11-Okt-2025', - no_referensi: 'PO-OVK-340', - qty_masuk: 38, - qty_keluar: 0, - qty_pakai: 38, - uraian: 'TILOXIN SOLUBLE', - kategori_produk: 'OVK OBAT', - harga_beli_per_qty: 200424, - total_harga: 7616112, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 5, - tanggal: '27-Sep-2025', - no_referensi: 'PO-OVK-364', - qty_masuk: 7, - qty_keluar: 0, - qty_pakai: 7, - uraian: 'EGG STIMULANT', - kategori_produk: 'OVK VITAMIN', - harga_beli_per_qty: 115024, - total_harga: 805168, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 6, - tanggal: '16-Sep-2025', - no_referensi: 'PO-OVK-982', - qty_masuk: 57, - qty_keluar: 0, - qty_pakai: 57, - uraian: 'MULTIVIT-AMINO', - kategori_produk: 'OVK VITAMIN', - harga_beli_per_qty: 65123, - total_harga: 3712011, - keterangan: 'Program kesehatan & biosecurity', - }, - { - id: 7, - tanggal: '04-Okt-2025', - no_referensi: 'PO-OVK-876', - qty_masuk: 4, - qty_keluar: 0, - qty_pakai: 4, - uraian: 'BKC DESINFEKTAN', - kategori_produk: 'OVK KIMIA', - harga_beli_per_qty: 105677, - total_harga: 422708, - keterangan: 'Program kesehatan & biosecurity', - }, - ], - total: { - label: 'TOTAL OVK', - qty_masuk: 222, - qty_keluar: 0, - qty_pakai: 222, - harga_beli_per_qty: 176096.36, - total_harga: 39093192, - }, - }, - pakan: { - rows: [ - { - id: 1, - tanggal: '13-Ags-2025', - no_referensi: 'PO-FEED-730', - qty_masuk: 4833, - qty_keluar: 0, - qty_pakai: 4833, - uraian: 'FEED PRE-LAY', - kategori_produk: 'PAKAN PRE-LAY', - harga_beli_per_qty: 7578, - total_harga: 36625874, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 2, - tanggal: '28-Jul-2025', - no_referensi: 'PO-FEED-555', - qty_masuk: 6500, - qty_keluar: 0, - qty_pakai: 6500, - uraian: 'FEED LAYER PHASE 1', - kategori_produk: 'PAKAN LAYER', - harga_beli_per_qty: 8116, - total_harga: 52754000, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 3, - tanggal: '24-Agu-2025', - no_referensi: 'PO-FEED-683', - qty_masuk: 8802, - qty_keluar: 0, - qty_pakai: 8802, - uraian: 'FEED LAYER PHASE 2', - kategori_produk: 'PAKAN LAYER', - harga_beli_per_qty: 8801, - total_harga: 77465402, - keterangan: 'Konsumsi pakan kandang layer', - }, - { - id: 4, - tanggal: '02-Sep-2025', - no_referensi: 'PO-FEED-448', - qty_masuk: 2185, - qty_keluar: 0, - qty_pakai: 2185, - uraian: 'JAGUNG GILING', - kategori_produk: 'PAKAN MIX', - harga_beli_per_qty: 5573, - total_harga: 12187705, - keterangan: 'Konsumsi pakan kandang layer', - }, - ], - total: { - label: 'TOTAL PAKAN', - qty_masuk: 22320, - qty_keluar: 0, - qty_pakai: 22320, - harga_beli_per_qty: 8020.93, - total_harga: 179032981, - }, - }, -}; - -export default DUMMY_SAPRONAK_CALCULATION; 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 index 8f2290ee..9dc5ab30 100644 --- a/src/services/api/closing.ts +++ b/src/services/api/closing.ts @@ -1,60 +1,76 @@ -import DUMMY_SAPRONAK_CALCULATION from '@/dummy/closing.dummy'; -import { BaseApiService } from './base'; -import { BaseApiResponse } from '@/types/api/api-general'; -import { ClosingSales, SapronakCalculation } from '@/types/api/closing/closing'; +import axios from 'axios'; -export class ClosingApiService extends BaseApiService< - ClosingSales, - unknown, - unknown -> { +import { BaseApiService } from '@/services/api/base'; +import { + Closing, + ClosingGeneralInformation, + ClosingIncomingSapronak, + ClosingOutgoingSapronak, + ClosingSapronakCalculation, +} 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 getPenjualan( - id: number - ): Promise | undefined> { + async getAllIncomingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getAllOutgoingSapronakFetcher( + endpoint: string + ): Promise> { + return await httpClientFetcher>( + endpoint + ); + } + + async getGeneralInfo(id: number) { try { - const getPenjualanPath = `${id}/penjualan`; - return await this.customRequest>( - getPenjualanPath - ); - } catch { + const getGeneralInfoPath = `${this.basePath}/${id}`; + const getGeneralInfoRes = + await httpClient>( + getGeneralInfoPath + ); + + return getGeneralInfoRes; + } catch (error) { + if ( + axios.isAxiosError>(error) + ) { + return error.response?.data; + } return undefined; } } async getPerhitunganSapronak( - projectFlockId: number - ): Promise | undefined> { - // Dummy implementation - simulate API call with delay - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - code: 200, - status: 'success', - message: 'Retrieved sapronak calculation successfully', - data: DUMMY_SAPRONAK_CALCULATION, - }); - }, 500); // Simulate 500ms network delay - }); - - /* - // Real API implementation - uncomment when backend is ready + id: number + ): Promise | undefined> { try { - const path = `${this.basePath}/${projectFlockId}/perhitungan_sapronak`; - - return await httpClient>(path, { - method: 'GET', - }); - } catch (error: unknown) { - if (axios.isAxiosError>(error)) { + const path = `${this.basePath}/${id}/perhitungan_sapronak`; + + return await httpClient>( + path, + { + method: 'GET', + } + ); + } catch (error) { + if ( + axios.isAxiosError>(error) + ) { return error.response?.data; } return undefined; } - */ } } diff --git a/src/styles/daisyui.css b/src/styles/daisyui.css index fc87399f..8eca2c82 100644 --- a/src/styles/daisyui.css +++ b/src/styles/daisyui.css @@ -1,4 +1,9 @@ @layer utilities { + .menu { + --menu-active-fg: var(--color-primary); + --menu-active-bg: transparent; + } + .step.step-success::before { --step-bg: var(--color-success); --step-fg: var(--color-success-content); diff --git a/src/types/api/closing.d.ts b/src/types/api/closing.d.ts new file mode 100644 index 00000000..0c4c84df --- /dev/null +++ b/src/types/api/closing.d.ts @@ -0,0 +1,91 @@ +import { Area } from '@/types/api/master-data/area'; +import { Fcr } from '@/types/api/master-data/fcr'; +import { Flock } from '@/types/api/master-data/flock'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { Location } from '@/types/api/master-data/location'; +import { BaseApproval, BaseMetadata } from '@/types/api/api-general'; + +export type BaseClosing = { + id: number; + location_id: number; + location_name: string; + project_category: 'GROWING' | 'LAYING'; + period: number; + closing_date?: string; + shed_label: string; + shed_count: number; + sales_paid_amount: number; + sales_remaining_amount: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; +}; + +export type Closing = BaseMetadata & BaseClosing; + +export type BaseClosingGeneralInformation = BaseClosing & { + flock_id: number; + period: number; + project_type: 'GROWING' | 'LAYING'; + population: number; + active_house_count: number; + sales_payment_status: string; + project_status: 'Pengajuan' | 'Aktif' | 'Selesai'; + closing_status: string; +}; + +export type ClosingGeneralInformation = BaseMetadata & + BaseClosingGeneralInformation; + +export type ClosingIncomingSapronak = { + id: number; + date: string; + reference_number: string; + transaction_type: string; + product_name: string; + product_category: string; + product_sub_category: string; + source_warehouse: string; + destination_warehouse: string; + quantity: number; + unit: string; + formatted_quantity: string; + notes: string; +}; + +export type ClosingOutgoingSapronak = ClosingIncomingSapronak; + +// ====== PERHITUNGAN SAPRONAK ====== + +export type RowSapronakCalculation = { + id: number; + tanggal: string; + no_referensi: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + uraian: string; + kategori_produk: string; + harga_beli_per_qty: number; + total_harga: number; + keterangan: string; +}; + +export type TotalSapronakCalculation = { + label: string; + qty_masuk: number; + qty_keluar: number; + qty_pakai: number; + harga_beli_per_qty: number; + total_harga: number; +}; + +export type ClosingSapronakCalculationItem = { + rows: RowSapronakCalculation[]; + total: TotalSapronakCalculation; +}; + +export type ClosingSapronakCalculation = { + doc_broiler: ClosingSapronakCalculationItem; + ovk: ClosingSapronakCalculationItem; + pakan: ClosingSapronakCalculationItem; +}; diff --git a/src/types/api/closing/closing.d.ts b/src/types/api/closing/closing.d.ts deleted file mode 100644 index c56afb78..00000000 --- a/src/types/api/closing/closing.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { BaseMetadata } from '@/types/api/api-general'; -import { Product } from '@type/api/master-data/product'; -import { Customer } from '@type/api/master-data/customer'; -import { Kandang } from '@type/api/master-data/kandang'; - -export type BaseSales = { - id: number; - realization_date: string; - age: number; - do_number: string; - product: Product; - customer: Customer; - qty: number; - weight: number; - avg_weight: number; - price: number; - total_price: number; - kandang: Kandang; - payment_status: string; -}; - -export type BaseClosingSales = { - project_type: string; - flock_id: number; - period: number; - sales: BaseSales[]; -}; - -export type ClosingSales = BaseMetadata & BaseClosingSales; - -// ====== PERHITUNGAN SAPRONAK ====== - -export type RowSapronakCalculation = { - id: number; - tanggal: string; - no_referensi: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - uraian: string; - kategori_produk: string; - harga_beli_per_qty: number; - total_harga: number; - keterangan: string; -}; - -export type TotalSapronakCalculation = { - label: string; - qty_masuk: number; - qty_keluar: number; - qty_pakai: number; - harga_beli_per_qty: number; - total_harga: number; -}; - -export type SapronakCalculationItem = { - rows: RowSapronakCalculation[]; - total: TotalSapronakCalculation; -}; - -export type SapronakCalculation = { - doc_broiler: SapronakCalculationItem; - ovk: SapronakCalculationItem; - pakan: SapronakCalculationItem; -}; 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';