From e1569c607cc4c125f5a83ef04489a06d26d76e53 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 7 Oct 2025 21:22:05 +0700 Subject: [PATCH] feat(FE-41,43): add Products table with CRUD operations and search functionality --- .../master-data/product/ProductTable.tsx | 350 ++++++++++++++++++ 1 file changed, 350 insertions(+) create mode 100644 src/components/pages/master-data/product/ProductTable.tsx diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx new file mode 100644 index 00000000..ab256548 --- /dev/null +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -0,0 +1,350 @@ +'use client'; + +import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { + CellContext, + ColumnDef, + ColumnSort, + SortingState, +} from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import DebouncedTextInput from '@/components/input/DebouncedTextInput'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import SelectInput, { OptionType } from '@/components/input/SelectInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; + +import { Product } from '@/types/api/master-data/product'; +import { ProductApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { ROWS_OPTIONS } from '@/config/constant'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => ( +
+ + + +
+); + +const ProductsTable = () => { + const { + state: tableFilterState, + updateFilter, + setPage, + setPageSize, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + nameSort: '', + skuSort: '', + brandSort: '', + categorySort: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + nameSort: 'sort_name', + skuSort: 'sort_sku', + brandSort: 'sort_brand', + categorySort: 'sort_category', + }, + }); + + const { + data: products, + isLoading, + mutate: refreshProducts, + } = useSWR( + `${ProductApi.basePath}${getTableFilterQueryString()}`, + ProductApi.getAllFetcher + ); + + const deleteModal = useModal(); + const [selectedProduct, setSelectedProduct] = useState(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [sorting, setSorting] = useState([]); + + const productsColumns: ColumnDef[] = [ + { + header: '#', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'sku', + header: 'SKU', + }, + { + accessorKey: 'brand', + header: 'Merek', + }, + { + accessorKey: 'product_category', + header: 'Kategori', + cell: (props) => props.row.original.product_category?.name ?? '-', + }, + { + accessorKey: 'uom', + header: 'Satuan', + cell: (props) => props.row.original.uom?.name ?? '-', + }, + { + accessorKey: 'product_price', + header: 'Harga Produk', + cell: (props) => props.row.original.product_price?.toLocaleString() ?? '-', + }, + { + accessorKey: 'selling_price', + header: 'Harga Jual', + cell: (props) => props.row.original.selling_price?.toLocaleString() ?? '-', + }, + { + accessorKey: 'tax', + header: 'Pajak (%)', + cell: (props) => props.row.original.tax ?? '-', + }, + { + accessorKey: 'expiry_period', + header: 'Kadaluarsa (hari)', + cell: (props) => props.row.original.expiry_period ?? '-', + }, + { + accessorKey: 'suppliers', + header: 'Supplier', + cell: (props) => + props.row.original.suppliers?.map((s) => s.name).join(', ') || '-', + }, + { + accessorKey: 'flags', + header: 'Flags', + cell: (props) => + props.row.original.flags?.length + ? props.row.original.flags.join(', ') + : '-', + }, + { + 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 - 2; + + const deleteClickHandler = () => { + setSelectedProduct(props.row.original); + deleteModal.openModal(); + }; + + return ( + <> + {currentPageSize > 2 && ( + + + + )} + {currentPageSize <= 2 && ( + + + + )} + + ); + }, + }, + ]; + + const confirmationModalDeleteClickHandler = async () => { + setIsDeleteLoading(true); + await ProductApi.delete(selectedProduct?.id as number); + refreshProducts(); + deleteModal.closeModal(); + toast.success('Successfully delete Product!'); + setIsDeleteLoading(false); + }; + + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; + + const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { + const newVal = val as OptionType; + setPageSize(newVal.value as number); + }; + + const updateSortingFilter = useCallback( + ( + sortName: Exclude, + sortFilter: ColumnSort | undefined + ) => { + if (!sortFilter) { + updateFilter(sortName, ''); + } else { + updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); + } + }, + [updateFilter] + ); + + useEffect(() => { + const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); + const skuSortFilter = sorting.find((sortItem) => sortItem.id === 'sku'); + const brandSortFilter = sorting.find((sortItem) => sortItem.id === 'brand'); + const categorySortFilter = sorting.find((sortItem) => sortItem.id === 'product_category'); + + updateSortingFilter('nameSort', nameSortFilter); + updateSortingFilter('skuSort', skuSortFilter); + updateSortingFilter('brandSort', brandSortFilter); + updateSortingFilter('categorySort', categorySortFilter); + }, [sorting]); + + return ( + <> +
+
+
+
+ +
+ +
+
+ +
+
+ + data={isResponseSuccess(products) ? products?.data : []} + columns={productsColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(products) ? products?.meta?.page : 0} + totalItems={ + isResponseSuccess(products) ? products?.meta?.total_results : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn({ + 'mb-20': + isResponseSuccess(products) && products?.data?.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', + }} + /> +
+ + + ); +}; + +export default ProductsTable; \ No newline at end of file