diff --git a/src/app/finance/page.tsx b/src/app/finance/page.tsx index ec78820c..11a67181 100644 --- a/src/app/finance/page.tsx +++ b/src/app/finance/page.tsx @@ -3,12 +3,7 @@ import FinanceTable from '@/components/pages/finance/FinanceTable'; const Finance = () => { - return ( -
-
- -
- ); + return ; }; export default Finance; diff --git a/src/app/master-data/area/page.tsx b/src/app/master-data/area/page.tsx index f8789af2..2c3cef14 100644 --- a/src/app/master-data/area/page.tsx +++ b/src/app/master-data/area/page.tsx @@ -1,11 +1,7 @@ import AreasTable from '@/components/pages/master-data/area/AreasTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/bank/page.tsx b/src/app/master-data/bank/page.tsx index 3f913c55..371cc3bf 100644 --- a/src/app/master-data/bank/page.tsx +++ b/src/app/master-data/bank/page.tsx @@ -1,11 +1,7 @@ import BanksTable from '@/components/pages/master-data/bank/BanksTable'; const Bank = () => { - return ( -
- -
- ); + return ; }; export default Bank; diff --git a/src/app/master-data/customer/page.tsx b/src/app/master-data/customer/page.tsx index 8aec1088..05c0e1e8 100644 --- a/src/app/master-data/customer/page.tsx +++ b/src/app/master-data/customer/page.tsx @@ -1,11 +1,7 @@ import CustomersTable from '@/components/pages/master-data/customer/CustomersTable'; const Customer = () => { - return ( -
- -
- ); + return ; }; export default Customer; diff --git a/src/app/master-data/flock/page.tsx b/src/app/master-data/flock/page.tsx index 76cc32c1..418018ab 100644 --- a/src/app/master-data/flock/page.tsx +++ b/src/app/master-data/flock/page.tsx @@ -1,11 +1,7 @@ import FlockTable from '@/components/pages/master-data/flock/FlocksTable'; const Flock = () => { - return ( -
- -
- ); + return ; }; export default Flock; diff --git a/src/app/master-data/kandang/page.tsx b/src/app/master-data/kandang/page.tsx index 293eb0da..e281e82c 100644 --- a/src/app/master-data/kandang/page.tsx +++ b/src/app/master-data/kandang/page.tsx @@ -1,11 +1,7 @@ import KandangsTable from '@/components/pages/master-data/kandang/KandangsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/location/page.tsx b/src/app/master-data/location/page.tsx index 338fdbff..af65761f 100644 --- a/src/app/master-data/location/page.tsx +++ b/src/app/master-data/location/page.tsx @@ -1,11 +1,7 @@ import LocationsTable from '@/components/pages/master-data/location/LocationsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/nonstock/page.tsx b/src/app/master-data/nonstock/page.tsx index 0812a5e2..02ed2e1e 100644 --- a/src/app/master-data/nonstock/page.tsx +++ b/src/app/master-data/nonstock/page.tsx @@ -1,11 +1,7 @@ import NonstocksTable from '@/components/pages/master-data/nonstock/NonstocksTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/product-category/page.tsx b/src/app/master-data/product-category/page.tsx index 78a4fda3..7c0a6656 100644 --- a/src/app/master-data/product-category/page.tsx +++ b/src/app/master-data/product-category/page.tsx @@ -1,11 +1,7 @@ import ProductCategoryTable from '@/components/pages/master-data/product-category/ProductCategoryTable'; const ProductCategory = () => { - return ( -
- -
- ); + return ; }; export default ProductCategory; diff --git a/src/app/master-data/product/page.tsx b/src/app/master-data/product/page.tsx index a385d411..5a4aafa9 100644 --- a/src/app/master-data/product/page.tsx +++ b/src/app/master-data/product/page.tsx @@ -1,11 +1,7 @@ import ProductsTable from '@/components/pages/master-data/product/ProductTable'; const Product = () => { - return ( -
- -
- ); + return ; }; export default Product; diff --git a/src/app/master-data/production-standard/page.tsx b/src/app/master-data/production-standard/page.tsx index ed1107cd..17944ebe 100644 --- a/src/app/master-data/production-standard/page.tsx +++ b/src/app/master-data/production-standard/page.tsx @@ -1,11 +1,7 @@ import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable'; const ProductionStandardPage = () => { - return ( -
- -
- ); + return ; }; export default ProductionStandardPage; diff --git a/src/app/master-data/supplier/page.tsx b/src/app/master-data/supplier/page.tsx index 8000be0a..169fd071 100644 --- a/src/app/master-data/supplier/page.tsx +++ b/src/app/master-data/supplier/page.tsx @@ -1,11 +1,7 @@ import SuppliersTable from '@/components/pages/master-data/supplier/SupplierTable'; const Supplier = () => { - return ( -
- -
- ); + return ; }; export default Supplier; diff --git a/src/app/master-data/uom/page.tsx b/src/app/master-data/uom/page.tsx index 689b9d0d..b5ba52b8 100644 --- a/src/app/master-data/uom/page.tsx +++ b/src/app/master-data/uom/page.tsx @@ -1,11 +1,7 @@ import UomsTable from '@/components/pages/master-data/uom/UomsTable'; const Nonstock = () => { - return ( -
- -
- ); + return ; }; export default Nonstock; diff --git a/src/app/master-data/warehouse/page.tsx b/src/app/master-data/warehouse/page.tsx index eb5ae416..7119283e 100644 --- a/src/app/master-data/warehouse/page.tsx +++ b/src/app/master-data/warehouse/page.tsx @@ -1,11 +1,7 @@ import WarehousesTable from '@/components/pages/master-data/warehouse/WarehousesTable'; const Warehouse = () => { - return ( -
- -
- ); + return ; }; export default Warehouse; diff --git a/src/components/pages/closing/ClosingsTable.tsx b/src/components/pages/closing/ClosingsTable.tsx index 7885c75c..912e8dfd 100644 --- a/src/components/pages/closing/ClosingsTable.tsx +++ b/src/components/pages/closing/ClosingsTable.tsx @@ -351,19 +351,19 @@ const ClosingsTable = () => { ) : data.length === 0 ? ( - - } - title='Data Closing Belum Tersedia' - subtitle='Tidak ada data closing untuk saat ini.' - /> +
+ + } + /> +
) : ( data={isResponseSuccess(closings) ? closings?.data : []} @@ -382,10 +382,7 @@ const ClosingsTable = () => { rowSelection={rowSelection} setRowSelection={setRowSelection} className={{ - containerClassName: cn('mt-3', { - 'w-full mb-0': - isResponseSuccess(closings) && closings?.data?.length === 0, - }), + containerClassName: cn('mt-3 mb-0'), headerColumnClassName: 'text-nowrap', }} /> diff --git a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx index 4b59510a..c99a3194 100644 --- a/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx +++ b/src/components/pages/closing/skeleton/ClosingTableSkeleton.tsx @@ -6,13 +6,13 @@ import { ColumnDef } from '@tanstack/react-table'; const ClosingTableSkeleton = ({ columns, icon, - title, - subtitle, + title = 'No Data Available', + subtitle = 'There is no closing data displayed. Enter closing data to get started.', }: { columns: ColumnDef[]; icon: React.ReactNode; - title: string; - subtitle: string; + title?: string; + subtitle?: string; }) => { return (
diff --git a/src/components/pages/expense/ExpensesTable.tsx b/src/components/pages/expense/ExpensesTable.tsx index e2e86535..849c1f83 100644 --- a/src/components/pages/expense/ExpensesTable.tsx +++ b/src/components/pages/expense/ExpensesTable.tsx @@ -25,6 +25,7 @@ import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWith import RequirePermission from '@/components/helper/RequirePermission'; import ButtonFilter from '@/components/helper/ButtonFilter'; import ExpensesFilterModal from '@/components/pages/expense/filter/ExpensesFilterModal'; +import ExpenseTableSkeleton from '@/components/pages/expense/skeleton/ExpenseTableSkeleton'; import { Expense } from '@/types/api/expense'; import { ExpenseApi } from '@/services/api/expense'; @@ -692,30 +693,47 @@ const ExpensesTable = () => { {/* Table Section */}
- - data={isResponseSuccess(expenses) ? expenses?.data : []} - columns={expensesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} - totalItems={ - isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(expenses) && expenses?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(expenses) || expenses.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(expenses) ? expenses?.data : []} + columns={expensesColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(expenses) ? expenses?.meta?.page : 0} + totalItems={ + isResponseSuccess(expenses) ? expenses?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
diff --git a/src/components/pages/expense/filter/ExpensesFilterModal.tsx b/src/components/pages/expense/filter/ExpensesFilterModal.tsx index 99f5a75a..1885785f 100644 --- a/src/components/pages/expense/filter/ExpensesFilterModal.tsx +++ b/src/components/pages/expense/filter/ExpensesFilterModal.tsx @@ -121,37 +121,40 @@ const ExpensesFilterModal = ({ {/* Modal Body */}
- - - - {formik.touched.realization_date && - formik.errors.realization_date && ( - - {formik.errors.realization_date} - - )} +
+ Tanggal +
+ +
+ +
+ {formik.touched.realization_date && + formik.errors.realization_date && ( + + {formik.errors.realization_date} + + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+ +
+ +
+ + ); +}; + +export default ExpenseTableSkeleton; diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index b30308a5..100eecb4 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -1,10 +1,19 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { CellContext } from '@tanstack/react-table'; +'use client'; + +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { CellContext, ColumnDef } from '@tanstack/react-table'; import useSWR from 'swr'; +import { Icon } from '@iconify/react'; import { useFormik } from 'formik'; +import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import Button from '@/components/Button'; -import Card from '@/components/Card'; import DateInput from '@/components/input/DateInput'; import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import SelectInput, { @@ -12,7 +21,6 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; import Table from '@/components/Table'; -import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { Finance } from '@/types/api/finance/finance'; import { @@ -25,115 +33,145 @@ import { FinanceApi } from '@/services/api/finance'; import { isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; -import { useModal } from '@/components/Modal'; +import Modal, { useModal } from '@/components/Modal'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { Icon } from '@iconify/react'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; import { useUiStore } from '@/stores/ui/ui.store'; import { FinanceTableFilterSchema, FinanceTableFilterValues, -} from './FinanceTableFilter.schema'; +} from '@/components/pages/finance/filter/FinanceFilter'; +import FinanceTableSkeleton from '@/components/pages/finance/skeleton/FinanceTableSkeleton'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `finance#${props.row.original.id}`; + const popoverAnchorName = `--anchor-finance#${props.row.original.id}`; + + const closePopover = () => { + const popover = document.getElementById(popoverId) as + | HTMLDivElement + | undefined; + popover?.hidePopover?.(); + }; + return ( - - + - - + + - {FINANCE_TRANSACTION_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + + - {FINANCE_INITIAL_BALANCE_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + {FINANCE_TRANSACTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} - {FINANCE_INJECTION_STATUS.includes( - props.row.original.transaction_type - ) && ( - - - - )} + {FINANCE_INITIAL_BALANCE_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} - - - - + {FINANCE_INJECTION_STATUS.includes( + props.row.original.transaction_type + ) && ( + + + + )} + + + + + + + ); }; @@ -171,6 +209,9 @@ const FinanceTable = () => { }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + // ===== State ===== const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< @@ -215,6 +256,18 @@ const FinanceTable = () => { updateFilter('sortBy', values.sort_by); updateFilter('startDate', values.start_date); updateFilter('endDate', values.end_date); + filterModal.closeModal(); + }, + onReset: () => { + updateFilter('search', ''); + resetSearchValue(); + updateFilter('transactionTypes', ''); + updateFilter('bankIds', ''); + updateFilter('customerIds', ''); + updateFilter('supplierIds', ''); + updateFilter('sortBy', ''); + updateFilter('startDate', ''); + updateFilter('endDate', ''); }, }); @@ -267,10 +320,41 @@ const FinanceTable = () => { }); }, [bankOptions, bankRawData]); + // ===== ACTIVE FILTERS COUNT ===== + const activeFiltersCount = useMemo(() => { + let count = 0; + + if (tableFilterState.transactionTypes) count += 1; + if (tableFilterState.bankIds) count += 1; + if (tableFilterState.customerIds) count += 1; + if (tableFilterState.supplierIds) count += 1; + if (tableFilterState.sortBy) count += 1; + if (tableFilterState.startDate) count += 1; + if (tableFilterState.endDate) count += 1; + + return count; + }, [ + tableFilterState.transactionTypes, + tableFilterState.bankIds, + tableFilterState.customerIds, + tableFilterState.supplierIds, + tableFilterState.sortBy, + tableFilterState.startDate, + tableFilterState.endDate, + ]); + + const hasFilters = activeFiltersCount > 0; + // ===== Handler ===== - const searchChangeHandler = (e: React.ChangeEvent) => { - filterFormik.setFieldValue('search', e.target.value); - }; + const searchChangeHandler = useCallback( + (e: React.ChangeEvent) => { + updateFilter('search', e.target.value); + setSearchValue(e.target.value); + setPage(1); + }, + [updateFilter, setSearchValue, setPage] + ); + const transactionTypeChangeHandler = ( val: OptionType | OptionType[] | null ) => { @@ -384,6 +468,11 @@ const FinanceTable = () => { } }; + const handleFilterModalOpen = () => { + filterModal.openModal(); + filterFormik.validateForm(); + }; + const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -403,6 +492,7 @@ const FinanceTable = () => { updateFilter('startDate', ''); updateFilter('endDate', ''); }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -414,8 +504,8 @@ const FinanceTable = () => { setIsDeleteLoading(false); }; - const columns = useMemo(() => { - return [ + const columns: ColumnDef[] = useMemo( + () => [ { header: 'ID', accessorKey: 'payment_code', @@ -495,32 +585,17 @@ const FinanceTable = () => { }; return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - + ); }, }, - ]; - }, []); + ], + [] + ); useEffect(() => { return () => { @@ -552,152 +627,280 @@ const FinanceTable = () => { }, [resetSearchValue, dateErrorShown]); return ( -
-
- - - - - - - - - -
- + <> +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + + + + + + + +
+ + {/* Search and Filter */} +
+ + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', + }} + /> + -
- } - > -
- - - - - - - -
- - - data={isResponseSuccess(finances) ? finances.data : []} - columns={columns} - pageSize={tableFilterState.pageSize} - page={tableFilterState.page} - onPageChange={setPage} - onPageSizeChange={setPageSize} - totalItems={ - isResponseSuccess(finances) ? finances.meta?.total_results : 0 - } - isLoading={isLoading} - /> + + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(finances) || finances.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(finances) ? finances.data : []} + columns={columns} + pageSize={tableFilterState.pageSize} + page={tableFilterState.page} + totalItems={ + isResponseSuccess(finances) ? finances.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+
+ Tanggal +
+ +
+ +
+
+ + {}} + closeMenuOnSelect={false} + isClearable + isMulti + className={{ wrapper: 'w-full' }} + /> + + + + +
+ + {/* Modal Footer */} +
+ + +
+ +
+ { onClick: confirmationModalDeleteClickHandler, }} /> -
+ ); }; diff --git a/src/components/pages/finance/FinanceTableFilter.schema.ts b/src/components/pages/finance/filter/FinanceFilter.ts similarity index 100% rename from src/components/pages/finance/FinanceTableFilter.schema.ts rename to src/components/pages/finance/filter/FinanceFilter.ts diff --git a/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx b/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx new file mode 100644 index 00000000..ccfbf1f5 --- /dev/null +++ b/src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Finance } from '@/types/api/finance/finance'; +import { ColumnDef } from '@tanstack/react-table'; + +const FinanceTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no finance data displayed. Enter finance data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default FinanceTableSkeleton; diff --git a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx index 0ae5034b..6d8a17e2 100644 --- a/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx +++ b/src/components/pages/inventory/adjustment/InventoryAdjustmentTable.tsx @@ -13,6 +13,7 @@ import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; import StatusBadge from '@/components/helper/StatusBadge'; +import InventoryAdjustmentTableSkeleton from '@/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton'; const InventoryAdjustmentTable = () => { const { @@ -192,38 +193,55 @@ const InventoryAdjustmentTable = () => { {/* Table Section */}
- - data={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.data - : [] - } - columns={inventoryAdjustmentsColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(inventoryAdjustments) - ? inventoryAdjustments?.meta?.total_results - : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(inventoryAdjustments) && - inventoryAdjustments?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(inventoryAdjustments) || + inventoryAdjustments.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.data + : [] + } + columns={inventoryAdjustmentsColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(inventoryAdjustments) + ? inventoryAdjustments?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
); diff --git a/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx b/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx new file mode 100644 index 00000000..3473f996 --- /dev/null +++ b/src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { InventoryAdjustment } from '@/types/api/inventory/adjustment'; +import { ColumnDef } from '@tanstack/react-table'; + +const InventoryAdjustmentTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no inventory adjustment data displayed. Enter inventory adjustment data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default InventoryAdjustmentTableSkeleton; diff --git a/src/components/pages/inventory/movement/MovementTable.tsx b/src/components/pages/inventory/movement/MovementTable.tsx index ab4f80d0..c85577de 100644 --- a/src/components/pages/inventory/movement/MovementTable.tsx +++ b/src/components/pages/inventory/movement/MovementTable.tsx @@ -16,6 +16,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput'; import RequirePermission from '@/components/helper/RequirePermission'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; +import MovementTableSkeleton from '@/components/pages/inventory/movement/skeleton/MovementTableSkeleton'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -198,27 +199,44 @@ const MovementTable = () => { {/* Table Section */}
- - data={isResponseSuccess(movements) ? movements?.data : []} - columns={movementColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(movements) ? movements?.meta?.page : 0} - totalItems={ - isResponseSuccess(movements) ? movements?.meta?.total_results : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(movements) && movements?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(movements) || movements.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(movements) ? movements?.data : []} + columns={movementColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(movements) ? movements?.meta?.page : 0} + totalItems={ + isResponseSuccess(movements) ? movements?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
); diff --git a/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx b/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx new file mode 100644 index 00000000..a3ba3c5a --- /dev/null +++ b/src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Movement } from '@/types/api/inventory/movement'; +import { ColumnDef } from '@tanstack/react-table'; + +const MovementTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no movement data displayed. Enter movement data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default MovementTableSkeleton; diff --git a/src/components/pages/inventory/product/InventoryProductTable.tsx b/src/components/pages/inventory/product/InventoryProductTable.tsx index cfbc284a..53d8fcb3 100644 --- a/src/components/pages/inventory/product/InventoryProductTable.tsx +++ b/src/components/pages/inventory/product/InventoryProductTable.tsx @@ -15,6 +15,7 @@ import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; +import InventoryProductTableSkeleton from '@/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -206,36 +207,55 @@ const InventoryProductTable = () => { {/* Table Section */}
- - data={ - isResponseSuccess(inventoryProducts) ? inventoryProducts?.data : [] - } - columns={columns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(inventoryProducts) - ? inventoryProducts?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(inventoryProducts) - ? inventoryProducts?.meta?.total_results - : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(inventoryProducts) && - inventoryProducts?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(inventoryProducts) || + inventoryProducts.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(inventoryProducts) + ? inventoryProducts?.data + : [] + } + columns={columns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(inventoryProducts) + ? inventoryProducts?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(inventoryProducts) + ? inventoryProducts?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
); diff --git a/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx b/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx new file mode 100644 index 00000000..9fe9cb51 --- /dev/null +++ b/src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { InventoryProduct } from '@/types/api/inventory/product'; +import { ColumnDef } from '@tanstack/react-table'; + +const InventoryProductTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no inventory product data displayed. Enter inventory product data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default InventoryProductTableSkeleton; diff --git a/src/components/pages/marketing/MarketingTable.tsx b/src/components/pages/marketing/MarketingTable.tsx index de43ba68..540a3eca 100644 --- a/src/components/pages/marketing/MarketingTable.tsx +++ b/src/components/pages/marketing/MarketingTable.tsx @@ -31,6 +31,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import StatusBadge from '@/components/helper/StatusBadge'; import MarketingFilterModal from '@/components/pages/marketing/MarketingFilter'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import MarketingTableSkeleton from '@/components/pages/marketing/skeleton/MarketingTableSkeleton'; const RowsOptionsMenu = ({ props, @@ -616,28 +617,49 @@ const MarketingTable = () => { -
+
+ {isLoadingMarketing ? ( +
+ +
+ ) : !isResponseSuccess(marketing) || marketing.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( +
+ )} + []; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default MarketingTableSkeleton; diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index d92c7840..1884dca3 100644 --- a/src/components/pages/master-data/area/AreasTable.tsx +++ b/src/components/pages/master-data/area/AreasTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -11,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; import RequirePermission from '@/components/helper/RequirePermission'; +import AreaTableSkeleton from '@/components/pages/master-data/area/skeleton/AreaTableSkeleton'; import { Area } from '@/types/api/master-data/area'; import { AreaApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `area#${props.row.original.id}`; + const popoverAnchorName = `--anchor-area#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -87,10 +108,17 @@ const AreasTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '' }, - paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, }); + const [sorting, setSorting] = useState([]); + const { data: areas, isLoading, @@ -101,65 +129,12 @@ const AreasTable = () => { ); const deleteModal = useModal(); - const [selectedArea, setSelectedArea] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const areasColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - 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 = () => { - setSelectedArea(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -179,95 +154,132 @@ const AreasTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const areasColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedArea(props.row.original); + deleteModal.openModal(); + }; - // 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 ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
-
- - - -
-
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(areas) ? areas?.data : []} - columns={areasColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(areas) ? areas?.meta?.page : 0} - totalItems={isResponseSuccess(areas) ? areas?.meta?.total_results : 0} - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': isResponseSuccess(areas) && areas?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(areas) || areas.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(areas) ? areas?.data : []} + columns={areasColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(areas) ? areas?.meta?.page : 0} + totalItems={ + isResponseSuccess(areas) ? areas?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default AreaTableSkeleton; diff --git a/src/components/pages/master-data/bank/BanksTable.tsx b/src/components/pages/master-data/bank/BanksTable.tsx index c5a564fe..a6a5dbef 100644 --- a/src/components/pages/master-data/bank/BanksTable.tsx +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -11,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import BankTableSkeleton from '@/components/pages/master-data/bank/skeleton/BankTableSkeleton'; import { Bank } from '@/types/api/master-data/bank'; import { BankApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `bank#${props.row.original.id}`; + const popoverAnchorName = `--anchor-bank#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -87,10 +108,17 @@ const BanksTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '' }, - paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, }); + const [sorting, setSorting] = useState([]); + const { data: banks, isLoading, @@ -101,78 +129,12 @@ const BanksTable = () => { ); const deleteModal = useModal(); - const [selectedBank, setSelectedBank] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const banksColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'alias', - header: 'Alias', - }, - { - accessorKey: 'account_number', - header: 'No. Rekening', - }, - { - accessorKey: 'owner', - header: 'Pemilik', - cell: (props) => (props.getValue() ? props.getValue() : '-'), - }, - { - 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 = () => { - setSelectedBank(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -192,93 +154,145 @@ const BanksTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const banksColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'alias', + header: 'Alias', + }, + { + accessorKey: 'account_number', + header: 'No. Rekening', + }, + { + accessorKey: 'owner', + header: 'Pemilik', + cell: (props) => props.getValue() || '-', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedBank(props.row.original); + deleteModal.openModal(); + }; - // track sorting - useEffect(() => { - const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); - - if (!isNameSorted) { - updateFilter('nameSort', ''); - } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); - } - }, [sorting]); + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(banks) ? banks?.data : []} - columns={banksColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(banks) ? banks?.meta?.page : 0} - totalItems={isResponseSuccess(banks) ? banks?.meta?.total_results : 0} - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': isResponseSuccess(banks) && banks?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(banks) || banks.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(banks) ? banks?.data : []} + columns={banksColumns} + pageSize={tableFilterState.pageSize} + page={isResponseSuccess(banks) ? banks?.meta?.page : 0} + totalItems={ + isResponseSuccess(banks) ? banks?.meta?.total_results : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default BankTableSkeleton; diff --git a/src/components/pages/master-data/customer/CustomersTable.tsx b/src/components/pages/master-data/customer/CustomersTable.tsx index e605d9f7..2768daa3 100644 --- a/src/components/pages/master-data/customer/CustomersTable.tsx +++ b/src/components/pages/master-data/customer/CustomersTable.tsx @@ -1,77 +1,102 @@ 'use client'; -import Button from '@/components/Button'; +import { ChangeEventHandler, useMemo, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, 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 SelectInput, { OptionType } from '@/components/input/SelectInput'; +import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import Table from '@/components/Table'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { ROWS_OPTIONS } from '@/config/constant'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn } from '@/lib/helper'; -import { CustomerApi } from '@/services/api/master-data'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import CustomerTableSkeleton from '@/components/pages/master-data/customer/skeleton/CustomerTableSkeleton'; + import { Customer } from '@/types/api/master-data/customer'; -import { Icon } from '@iconify/react'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; -import { useState } from 'react'; -import toast from 'react-hot-toast'; -import useSWR from 'swr'; +import { CustomerApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `customer#${props.row.original.id}`; + const popoverAnchorName = `--anchor-customer#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - - - - - - - - +
+ + + + + +
+ + + + + + + + + +
+
+
); }; @@ -83,16 +108,17 @@ const CustomersTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '', picSort: '' }, + initial: { + search: '', + }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - picSort: 'sort_pic', }, }); - // Fetch Data + const [sorting, setSorting] = useState([]); + const { data: customers, isLoading, @@ -102,87 +128,16 @@ const CustomersTable = () => { CustomerApi.getAllFetcher ); - // State const deleteModal = useModal(); const [selectedCustomer, setSelectedCustomer] = useState< Customer | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // Columns Definition - const customersColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'pic', - header: 'PIC', - cell: (props) => props.row.original.pic.name, - }, - { - accessorKey: 'type', - header: 'Type', - cell: (props) => props.row.original.type, - }, - { - accessorKey: 'phone', - header: 'Phone', - }, - { - accessorKey: 'email', - header: 'Email', - }, - { - 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 searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - const deleteClickHandler = () => { - setSelectedCustomer(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; - - // Handler const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -202,81 +157,147 @@ const CustomersTable = () => { toast.success('Successfully delete Customer!'); setIsDeleteLoading(false); }; - const searchChangeHandler = (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; + + const customersColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorFn: (row) => row.pic?.name ?? '-', + header: 'PIC', + }, + { + accessorKey: 'type', + header: 'Type', + }, + { + accessorKey: 'phone', + header: 'Phone', + }, + { + accessorKey: 'email', + header: 'Email', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 = () => { + setSelectedCustomer(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
- - - -
- - +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + +
-
- + + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(customers) ? customers?.data : []} - columns={customersColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(customers) ? customers?.meta?.page : 0} - totalItems={ - isResponseSuccess(customers) ? customers?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(customers) && customers?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(customers) || customers.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={customers?.data} + columns={customersColumns} + pageSize={tableFilterState.pageSize} + page={customers?.meta?.page ?? 0} + totalItems={customers?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default CustomerTableSkeleton; diff --git a/src/components/pages/master-data/flock/FlocksTable.tsx b/src/components/pages/master-data/flock/FlocksTable.tsx index dd6ebfe8..3550a346 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -1,87 +1,102 @@ 'use client'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; -import { Flock } from '@/types/api/master-data/flock'; -import { cn } from '@/lib/helper'; -import Button from '@/components/Button'; -import { Icon } from '@iconify/react'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { FlockApi } from '@/services/api/master-data'; -import { useModal } from '@/components/Modal'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; -import RequirePermission from '@/components/helper/RequirePermission'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; -import DebouncedTextInput from '@/components/input/DebouncedTextInput'; -import SelectInput, { OptionType } from '@/components/input/SelectInput'; -import { ROWS_OPTIONS } from '@/config/constant'; -import Table from '@/components/Table'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -const RowsOptions = ({ - type = 'dropdown', +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 RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import FlockTableSkeleton from '@/components/pages/master-data/flock/skeleton/FlockTableSkeleton'; + +import { Flock } from '@/types/api/master-data/flock'; +import { FlockApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; + +const RowOptionsMenu = ({ + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `flock#${props.row.original.id}`; + const popoverAnchorName = `--anchor-flock#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - - - - - - - - +
+ + + + + +
+ + + + + + + + + +
+
+
); }; @@ -93,15 +108,17 @@ const FlockTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '' }, + initial: { + search: '', + }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', }, }); - // Fetch Data + const [sorting, setSorting] = useState([]); + const { data: flocks, isLoading, @@ -111,74 +128,16 @@ const FlockTable = () => { FlockApi.getAllFetcher ); - // State const deleteModal = useModal(); const [selectedFlock, setSelectedFlock] = useState( undefined ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // Columns Definition - const flocksColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'created_at', - header: 'Dibuat pada', - cell: (props) => - new Date(props.row.original.created_at).toLocaleDateString(), - }, - { - 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 searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - const deleteClickHandler = () => { - setSelectedFlock(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; - - // Handler const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -196,85 +155,143 @@ const FlockTable = () => { toast.success('Successfully delete Flock!'); setIsDeleteLoading(false); }; - const searchChangeHandler = (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; + + const flocksColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'created_at', + header: 'Dibuat pada', + cell: (props) => + new Date(props.row.original.created_at).toLocaleDateString('id-ID'), + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 = () => { + setSelectedFlock(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(flocks) ? flocks?.data : []} - columns={flocksColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(flocks) ? flocks?.meta?.page : 0} - totalItems={ - isResponseSuccess(flocks) ? flocks?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - className={{ - containerClassName: cn({ - 'mb-20': isResponseSuccess(flocks) && flocks?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(flocks) || flocks.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={flocks?.data} + columns={flocksColumns} + pageSize={tableFilterState.pageSize} + page={flocks?.meta?.page ?? 0} + totalItems={flocks?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ []; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default FlockTableSkeleton; diff --git a/src/components/pages/master-data/kandang/KandangsTable.tsx b/src/components/pages/master-data/kandang/KandangsTable.tsx index 7d79d456..63e9fa58 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -1,13 +1,8 @@ 'use client'; -import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { - CellContext, - ColumnDef, - ColumnSort, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -16,71 +11,93 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import KandangTableSkeleton from '@/components/pages/master-data/kandang/skeleton/KandangTableSkeleton'; import { Kandang } from '@/types/api/master-data/kandang'; import { KandangApi } from '@/services/api/master-data'; -import { cn, formatNumber } from '@/lib/helper'; +import { formatNumber } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `kandang#${props.row.original.id}`; + const popoverAnchorName = `--anchor-kandang#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -94,21 +111,15 @@ const KandangsTable = () => { } = useTableFilter({ initial: { search: '', - nameSort: '', - locationSort: '', - capacitySort: '', - picSort: '', }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - locationSort: 'sort_location', - capacitySort: 'sort_capacity', - picSort: ' sort_pic', }, }); + const [sorting, setSorting] = useState([]); + const { data: kandangs, isLoading, @@ -119,82 +130,14 @@ const KandangsTable = () => { ); const deleteModal = useModal(); - const [selectedKandang, setSelectedKandang] = useState( undefined ); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const kandangsColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'location', - header: 'Lokasi', - cell: (props) => props.row.original.location.name, - }, - { - accessorKey: 'capacity', - header: 'Kapasitas', - cell: (props) => formatNumber(props.row.original.capacity ?? 0), - }, - { - accessorKey: 'pic', - header: 'PIC', - cell: (props) => props.row.original.pic.name, - }, - { - 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 = () => { - setSelectedKandang(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -216,114 +159,143 @@ const KandangsTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const kandangsColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorFn: (row) => row.location?.name ?? '-', + header: 'Lokasi', + }, + { + accessorKey: 'capacity', + header: 'Kapasitas', + cell: (props) => formatNumber(props.row.original.capacity ?? 0), + }, + { + accessorFn: (row) => row.pic?.name ?? '-', + header: 'PIC', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedKandang(props.row.original); + deleteModal.openModal(); + }; - const updateSortingFilter = useCallback( - ( - sortName: Exclude, - sortFilter: ColumnSort | undefined - ) => { - if (!sortFilter) { - updateFilter(sortName, ''); - } else { - updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); - } - }, - [updateFilter] + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); - // track sorting - useEffect(() => { - const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); - const locationSortFilter = sorting.find( - (sortItem) => sortItem.id === 'location' - ); - const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic'); - - updateSortingFilter('nameSort', nameSortFilter); - updateSortingFilter('locationSort', locationSortFilter); - updateSortingFilter('picSort', picSortFilter); - }, [sorting, updateSortingFilter]); - return ( <> -
-
-
-
-
- - - -
-
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(kandangs) ? kandangs?.data : []} - columns={kandangsColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0} - totalItems={ - isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(kandangs) && kandangs?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(kandangs) || kandangs.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={kandangs?.data} + columns={kandangsColumns} + pageSize={tableFilterState.pageSize} + page={kandangs?.meta?.page ?? 0} + totalItems={kandangs?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default KandangTableSkeleton; diff --git a/src/components/pages/master-data/location/LocationsTable.tsx b/src/components/pages/master-data/location/LocationsTable.tsx index a35ffd09..0b619079 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -1,13 +1,8 @@ 'use client'; -import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { - CellContext, - ColumnDef, - ColumnSort, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -16,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import LocationTableSkeleton from '@/components/pages/master-data/location/skeleton/LocationTableSkeleton'; import { Location } from '@/types/api/master-data/location'; import { LocationApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `location#${props.row.original.id}`; + const popoverAnchorName = `--anchor-location#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -92,16 +108,17 @@ const LocationsTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '', addressSort: '', areaSort: '' }, + initial: { + search: '', + }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - addressSort: 'sort_address', - areaSort: ' sort_area', }, }); + const [sorting, setSorting] = useState([]); + const { data: locations, isLoading, @@ -112,76 +129,14 @@ const LocationsTable = () => { ); const deleteModal = useModal(); - const [selectedLocation, setSelectedLocation] = useState< Location | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const locationsColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'address', - header: 'Alamat', - }, - { - accessorKey: 'area', - header: 'Area', - cell: (props) => props.row.original.area.name, - }, - { - 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 = () => { - setSelectedLocation(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -203,114 +158,138 @@ const LocationsTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const locationsColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'address', + header: 'Alamat', + }, + { + accessorFn: (row) => row.area?.name ?? '-', + header: 'Area', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedLocation(props.row.original); + deleteModal.openModal(); + }; - const updateSortingFilter = useCallback( - ( - sortName: Exclude, - sortFilter: ColumnSort | undefined - ) => { - if (!sortFilter) { - updateFilter(sortName, ''); - } else { - updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); - } - }, - [updateFilter] + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); - // track sorting - useEffect(() => { - const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); - const addressSortFilter = sorting.find( - (sortItem) => sortItem.id === 'address' - ); - const areaSortFilter = sorting.find((sortItem) => sortItem.id === 'area'); - - updateSortingFilter('nameSort', nameSortFilter); - updateSortingFilter('addressSort', addressSortFilter); - updateSortingFilter('areaSort', areaSortFilter); - }, [sorting, updateSortingFilter]); - return ( <> -
-
-
-
-
- - - -
-
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(locations) ? locations?.data : []} - columns={locationsColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(locations) ? locations?.meta?.page : 0} - totalItems={ - isResponseSuccess(locations) ? locations?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(locations) && locations?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(locations) || locations.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={locations?.data} + columns={locationsColumns} + pageSize={tableFilterState.pageSize} + page={locations?.meta?.page ?? 0} + totalItems={locations?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default LocationTableSkeleton; diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 6aeb3f99..8f15e529 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -1,13 +1,8 @@ 'use client'; -import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { - CellContext, - ColumnDef, - ColumnSort, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -16,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import NonstockTableSkeleton from '@/components/pages/master-data/nonstock/skeleton/NonstockTableSkeleton'; import { Nonstock } from '@/types/api/master-data/nonstock'; import { NonstockApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `nonstock#${props.row.original.id}`; + const popoverAnchorName = `--anchor-nonstock#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -92,16 +108,17 @@ const NonstocksTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '', locationSort: '', picSort: '' }, + initial: { + search: '', + }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - locationSort: 'sort_location', - picSort: ' sort_pic', }, }); + const [sorting, setSorting] = useState([]); + const { data: nonstocks, isLoading, @@ -112,88 +129,14 @@ const NonstocksTable = () => { ); const deleteModal = useModal(); - const [selectedNonstock, setSelectedNonstock] = useState< Nonstock | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const nonstocksColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'uom', - header: 'UOM', - cell: (props) => props.row.original.uom.name, - }, - { - accessorKey: 'suppliers', - header: 'Supplier', - cell: (props) => { - const supplierNames = props.row.original.suppliers.map( - (supplier) => supplier.name - ); - - return supplierNames.join(', ') || '-'; - }, - }, - { - accessorKey: 'flags', - header: 'Flag', - cell: (props) => 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 = () => { - setSelectedNonstock(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -215,112 +158,143 @@ const NonstocksTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const nonstocksColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorFn: (row) => row.uom?.name ?? '-', + header: 'UOM', + }, + { + accessorFn: (row) => + row.suppliers?.map((supplier) => supplier.name).join(', ') || '-', + header: 'Supplier', + }, + { + accessorFn: (row) => row.flags?.join(', ') || '-', + header: 'Flag', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedNonstock(props.row.original); + deleteModal.openModal(); + }; - const updateSortingFilter = useCallback( - ( - sortName: Exclude, - sortFilter: ColumnSort | undefined - ) => { - if (!sortFilter) { - updateFilter(sortName, ''); - } else { - updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); - } - }, - [updateFilter] + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); - // track sorting - useEffect(() => { - const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); - const locationSortFilter = sorting.find( - (sortItem) => sortItem.id === 'location' - ); - const picSortFilter = sorting.find((sortItem) => sortItem.id === 'pic'); - - updateSortingFilter('nameSort', nameSortFilter); - updateSortingFilter('locationSort', locationSortFilter); - updateSortingFilter('picSort', picSortFilter); - }, [sorting]); - return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(nonstocks) ? nonstocks?.data : []} - columns={nonstocksColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(nonstocks) ? nonstocks?.meta?.page : 0} - totalItems={ - isResponseSuccess(nonstocks) ? nonstocks?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(nonstocks) && nonstocks?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(nonstocks) || nonstocks.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={nonstocks?.data} + columns={nonstocksColumns} + pageSize={tableFilterState.pageSize} + page={nonstocks?.meta?.page ?? 0} + totalItems={nonstocks?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
{ label: supplier.name, })) ?? [], - flags: initialValues?.flags ?? [], + flags: initialValues?.flags?.includes('EKSPEDISI') ?? false, }; }, [initialValues]); @@ -112,7 +112,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { name: values.name, uom_id: values.uomId, supplier_ids: values.supplierIds as number[], - flags: values.flags as flags[], + flags: values.flags ? ['EKSPEDISI'] : [], }; switch (type) { @@ -183,12 +183,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { router.push('/master-data/nonstock'); }; - const flagsChangeHandler = (val: OptionType | OptionType[] | null) => { - const formattedFlags = (val as OptionType[]).map( - (flag) => flag.value as string - ); - - formik.setFieldValue('flags', formattedFlags); + const expeditionChangeHandler = (e: React.ChangeEvent) => { + formik.setFieldValue('flags', e.target.value === 'true'); }; useEffect(() => { @@ -268,18 +264,19 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { isDisabled={type === 'detail'} /> - - formik.values.flags?.includes(opt.value) - )} - onChange={flagsChangeHandler} - options={SUPPLIER_FLAG_OPTIONS} +
diff --git a/src/components/pages/master-data/nonstock/skeleton/NonstockTableSkeleton.tsx b/src/components/pages/master-data/nonstock/skeleton/NonstockTableSkeleton.tsx new file mode 100644 index 00000000..b3801a75 --- /dev/null +++ b/src/components/pages/master-data/nonstock/skeleton/NonstockTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Nonstock } from '@/types/api/master-data/nonstock'; +import { ColumnDef } from '@tanstack/react-table'; + +const NonstockTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no nonstock data displayed. Enter nonstock data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default NonstockTableSkeleton; diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index 11199c73..161ebed4 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -1,6 +1,12 @@ 'use client'; -import { ChangeEventHandler, useEffect, useRef, useState } from 'react'; +import { + ChangeEventHandler, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -11,72 +17,93 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import ProductCategoryTableSkeleton from '@/components/pages/master-data/product-category/skeleton/ProductCategoryTableSkeleton'; import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { useUiStore } from '@/stores/ui/ui.store'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `product-category#${props.row.original.id}`; + const popoverAnchorName = `--anchor-product-category#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -91,10 +118,17 @@ const ProductCategoryTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: searchValue, nameSort: '' }, - paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + initial: { + search: searchValue, + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, }); + const [sorting, setSorting] = useState([]); + const { data: productCategories, isLoading, @@ -105,71 +139,15 @@ const ProductCategoryTable = () => { ); const deleteModal = useModal(); - const [selectedProductCategory, setSelectedProductCategory] = useState< ProductCategory | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const productCategoryColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'code', - header: 'Code', - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - 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 = () => { - setSelectedProductCategory(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + setSearchValue(e.target.value); + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -191,15 +169,51 @@ const ProductCategoryTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - setSearchValue(e.target.value); - updateFilter('search', e.target.value); - }; + const productCategoryColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'code', + header: 'Code', + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; + + const deleteClickHandler = () => { + setSelectedProductCategory(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); useEffect(() => { // Store current path on mount @@ -223,91 +237,91 @@ const ProductCategoryTable = () => { }; }, [resetSearchValue]); - useEffect(() => { - const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name'); - if (!isNameSorted) { - updateFilter('nameSort', ''); - } else { - updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc'); - } - }, [sorting, updateFilter]); - return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ + {/* Search */} +
-
-
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={ - isResponseSuccess(productCategories) ? productCategories?.data : [] - } - columns={productCategoryColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(productCategories) - ? productCategories?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(productCategories) - ? productCategories?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(productCategories) && - productCategories?.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', - }} - /> + + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(productCategories) || + productCategories.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={productCategories?.data} + columns={productCategoryColumns} + pageSize={tableFilterState.pageSize} + page={productCategories?.meta?.page ?? 0} + totalItems={productCategories?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ []; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProductCategoryTableSkeleton; diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index 74137a14..08b585f0 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -1,13 +1,8 @@ 'use client'; -import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { - CellContext, - ColumnDef, - ColumnSort, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -16,69 +11,95 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import ProductTableSkeleton from '@/components/pages/master-data/product/skeleton/ProductTableSkeleton'; import { Product } from '@/types/api/master-data/product'; import { ProductApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; +import { formatCurrency } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; -}) => ( - - - - - - - - - - - -); +
+ + + + + + + + + +
+ + + ); +}; const ProductsTable = () => { const { @@ -90,21 +111,15 @@ const ProductsTable = () => { } = 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 [sorting, setSorting] = useState([]); + const { data: products, isLoading, @@ -119,114 +134,10 @@ const ProductsTable = () => { 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 searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -248,110 +159,190 @@ const ProductsTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const productsColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'sku', + header: 'SKU', + }, + { + accessorKey: 'brand', + header: 'Merek', + }, + { + accessorFn: (row) => row.product_category?.name ?? '-', + header: 'Kategori', + }, + { + accessorFn: (row) => row.uom?.name ?? '-', + header: 'Satuan', + }, + { + accessorKey: 'product_price', + header: 'Harga Produk', + cell: (props) => + props.row.original.product_price + ? formatCurrency(props.row.original.product_price) + : '-', + }, + { + accessorKey: 'selling_price', + header: 'Harga Jual', + cell: (props) => + props.row.original.selling_price + ? formatCurrency(props.row.original.selling_price) + : '-', + }, + { + accessorKey: 'tax', + header: 'Pajak (%)', + cell: (props) => props.row.original.tax ?? '-', + }, + { + accessorKey: 'expiry_period', + header: 'Kadaluarsa (hari)', + cell: (props) => props.row.original.expiry_period ?? '-', + }, + { + accessorFn: (row) => + row.suppliers?.map((s) => s.name).join(', ') || '-', + header: 'Supplier', + }, + { + accessorKey: 'flag', + header: 'Flag', + cell: (props) => + props.row.original.flag ? props.row.original.flag : '-', + }, + { + accessorFn: (row) => + row.sub_flags?.length ? row.sub_flags.join(', ') : '-', + header: 'Kategori Flags', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - const updateSortingFilter = useCallback( - ( - sortName: Exclude, - sortFilter: ColumnSort | undefined - ) => { - if (!sortFilter) { - updateFilter(sortName, ''); - } else { - updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); - } - }, - [updateFilter] + const deleteClickHandler = () => { + setSelectedProduct(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); - 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, updateSortingFilter]); - return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ + {/* Search */} +
-
-
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - 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', - }} - /> + + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(products) || products.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={products?.data} + columns={productsColumns} + pageSize={tableFilterState.pageSize} + page={products?.meta?.page ?? 0} + totalItems={products?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ = @@ -94,10 +96,26 @@ export const ProductFormSchema: Yup.ObjectSchema = ) .required('Supplier wajib diisi!'), - flags: Yup.array() + flag: Yup.string() + .min(1, 'Flag wajib diisi!') + .required('Flag wajib diisi!') + .typeError('Flag wajib diisi!'), + + sub_flags: Yup.array() .of(Yup.string().required()) - .min(1, 'Minimal harus ada 1 flag!') - .required('Flag wajib diisi!'), + .when('flag', { + is: (flag: string) => { + const mapping = PRODUCT_FLAG_MAPPING.options.find( + (opt) => opt.flag.value === flag + ); + return mapping?.allow_without_sub_flag === false; + }, + then: (schema) => + schema + .required('Sub flag wajib diisi!') + .min(1, 'Sub flag wajib diisi!'), + otherwise: (schema) => schema, + }), }); export const UpdateProductFormSchema = ProductFormSchema; diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 65329464..01fa192c 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -36,8 +36,16 @@ import { ProductApi, } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; + import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; +import { ConstantsApi } from '@/services/api/constants/constants'; +import type { + TransformedConstants, + ProductFlagMapping, +} from '@/types/api/constants/constants'; +import useSWR from 'swr'; +import { PRODUCT_FLAG_MAPPING } from '@/config/constant'; + import { Supplier } from '@/types/api/master-data/supplier'; import Card from '@/components/Card'; import { removeArrayItemAndSync } from '@/lib/utils/formik'; @@ -53,6 +61,24 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const { + data: constants, + error: constantsError, + isLoading: isLoadingConstants, + } = useSWR( + 'constants', + ConstantsApi.fetchTransformedConstants.bind(ConstantsApi), + { + shouldRetryOnError: false, + } + ); + + const productFlagMapping: ProductFlagMapping | null = useMemo(() => { + if (constantsError || !constants?.product_flag_mapping) { + return PRODUCT_FLAG_MAPPING as unknown as ProductFlagMapping; + } + return constants.product_flag_mapping; + }, [constants, constantsError]); const createProductHandler = useCallback( async (payload: CreateProductPayload) => { @@ -110,7 +136,8 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { price: supplier.price, })) : [], - flags: initialValues?.flags ?? [], + flag: initialValues?.flag ?? '', + sub_flags: initialValues?.sub_flags ?? [], }), [initialValues] ); @@ -139,7 +166,8 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { supplier_id: s.supplier?.value as number, price: parseInt(s.price.toString()) || 0, })), - flags: values.flags.filter((f): f is string => typeof f === 'string'), + flag: values.flag, + sub_flags: values.sub_flags, }; switch (type) { case 'add': @@ -200,6 +228,28 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { }); }, [supplierOptions, formik.values.suppliers]); + const selectedFlagMapping = useMemo(() => { + return productFlagMapping?.options.find( + (opt) => opt.flag.value === formik.values.flag + ); + }, [formik.values.flag, productFlagMapping]); + + const subFlagOptions = useMemo(() => { + return selectedFlagMapping?.sub_flags ?? []; + }, [selectedFlagMapping]); + + const selectedSubFlagValues = useMemo(() => { + return ( + selectedFlagMapping?.sub_flags.filter((subFlag) => + formik.values.sub_flags?.includes(subFlag.value as string) + ) ?? [] + ); + }, [selectedFlagMapping, formik.values.sub_flags]); + + const isSubFlagRequired = useMemo(() => { + return selectedFlagMapping?.allow_without_sub_flag === false; + }, [selectedFlagMapping]); + const addSupplierHandler = () => { formik.setFieldValue('suppliers', [ ...formik.values.suppliers, @@ -213,7 +263,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { const deleteSupplierItemHandler = (idx: number) => { const path = 'suppliers'; - // trims values, errors, and touched at idx removeArrayItemAndSync(formik, path, idx); }; @@ -428,26 +477,48 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { readOnly={type === 'detail'} />
-
+
- (formik.values.flags || []).includes(opt.value) + value={productFlagMapping?.flags.find( + (opt) => opt.value === formik.values.flag )} + onChange={(val) => { + const selectedFlag = String((val as OptionType)?.value ?? ''); + formik.setFieldValue('flag', selectedFlag); + formik.setFieldValue('sub_flags', []); + }} + options={productFlagMapping?.flags ?? []} + isLoading={isLoadingConstants && !productFlagMapping} + isError={formik.touched.flag && Boolean(formik.errors.flag)} + errorMessage={formik.errors.flag as string} + isDisabled={type === 'detail'} + isClearable + /> + + { const arr = Array.isArray(val) ? val : val ? [val] : []; formik.setFieldValue( - 'flags', - arr.map((v) => (v as OptionType).value) + 'sub_flags', + arr.map((v) => String((v as OptionType).value)) ); }} - options={PRODUCT_FLAG_OPTIONS} - isError={formik.touched.flags && Boolean(formik.errors.flags)} - errorMessage={formik.errors.flags as string} - isDisabled={type === 'detail'} + options={subFlagOptions} + isLoading={isLoadingConstants && !productFlagMapping} + isError={ + formik.touched.sub_flags && Boolean(formik.errors.sub_flags) + } + errorMessage={formik.errors.sub_flags as string} + isDisabled={type === 'detail' || !formik.values.flag} + closeMenuOnSelect={false} isClearable />
diff --git a/src/components/pages/master-data/product/skeleton/ProductTableSkeleton.tsx b/src/components/pages/master-data/product/skeleton/ProductTableSkeleton.tsx new file mode 100644 index 00000000..d0393421 --- /dev/null +++ b/src/components/pages/master-data/product/skeleton/ProductTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Product } from '@/types/api/master-data/product'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProductTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no product data displayed. Enter product data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProductTableSkeleton; diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index a8df6ae8..0ff8b594 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -1,92 +1,121 @@ 'use client'; -import Button from '@/components/Button'; -import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table'; -import { ProductionStandard } from '@/types/api/master-data/production-standard'; -import { Icon } from '@iconify/react'; +import { useMemo, useState } from 'react'; import useSWR from 'swr'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import toast from 'react-hot-toast'; + +import { Icon } from '@iconify/react'; +import Table from '@/components/Table'; +import Button from '@/components/Button'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import ProductionStandardTableSkeleton from '@/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton'; + +import { ProductionStandard } from '@/types/api/master-data/production-standard'; import { ProductionStandardApi } from '@/services/api/master-data'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; -import { CellContext } from '@tanstack/react-table'; -import { useModal } from '@/components/Modal'; -import { useState } from 'react'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import toast from 'react-hot-toast'; -import { cn } from '@/lib/helper'; -import RequirePermission from '@/components/helper/RequirePermission'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `production-standard#${props.row.original.id}`; + const popoverAnchorName = `--anchor-production-standard#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; const ProductionStandardTable = () => { - const deleteModal = useModal(); + const [sorting, setSorting] = useState([]); + const { + data: productionStandards, + isLoading, + mutate: refreshProductionStandards, + } = useSWR( + `${ProductionStandardApi.basePath}`, + ProductionStandardApi.getAllFetcher + ); + + const deleteModal = useModal(); const [selectedProductionStandard, setSelectedProductionStandard] = useState< ProductionStandard | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { data: productionStandards, mutate: refreshProductionStandards } = - useSWR( - `${ProductionStandardApi.basePath}`, - ProductionStandardApi.getAllFetcher - ); - const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -107,112 +136,120 @@ const ProductionStandardTable = () => { setIsDeleteLoading(false); }; + const productionStandardColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => props.row.index + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorFn: (row) => row.project_category ?? '-', + header: 'Kategori', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 = () => { + setSelectedProductionStandard(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [deleteModal] + ); + return ( <> -
-
- - - +
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
- - - data={ - isResponseSuccess(productionStandards) - ? productionStandards.data - : [] - } - columns={[ - { - header: 'No', - accessorFn: (row, index) => index + 1, - }, - { - header: 'Nama', - accessorKey: 'name', - }, - { - header: 'Kategori', - accessorFn: (row) => row.project_category, - }, - { - 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 = () => { - setSelectedProductionStandard(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]} - className={{ - headerColumnClassName: cn( - TABLE_DEFAULT_STYLING.headerColumnClassName, - 'last:flex last:flex-row last:justify-end' - ), - bodyColumnClassName: cn( - TABLE_DEFAULT_STYLING.bodyColumnClassName, - 'last:flex last:flex-row last:justify-end' - ), - }} - /> - + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(productionStandards) || + productionStandards.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={productionStandards.data} + columns={productionStandardColumns} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
- - - + + ); }; diff --git a/src/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton.tsx b/src/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton.tsx new file mode 100644 index 00000000..590b4479 --- /dev/null +++ b/src/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProductionStandard } from '@/types/api/master-data/production-standard'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProductionStandardTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no production standard data displayed. Enter production standard data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProductionStandardTableSkeleton; diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index 2620c9e6..2b6cb227 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -1,87 +1,102 @@ 'use client'; -import Button from '@/components/Button'; +import { ChangeEventHandler, useMemo, useState } from 'react'; +import useSWR from 'swr'; +import { CellContext, ColumnDef, 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 SelectInput, { OptionType } from '@/components/input/SelectInput'; +import Button from '@/components/Button'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; -import Table from '@/components/Table'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; -import { ROWS_OPTIONS } from '@/config/constant'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; -import { cn } from '@/lib/helper'; -import { SupplierApi } from '@/services/api/master-data'; -import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { Supplier } from '@/types/api/master-data/supplier'; -import { Icon } from '@iconify/react'; -import { CellContext, ColumnDef } from '@tanstack/react-table'; -import { useState } from 'react'; -import toast from 'react-hot-toast'; -import useSWR from 'swr'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton'; -const RowOptions = ({ - type = 'dropdown', +import { Supplier } from '@/types/api/master-data/supplier'; +import { SupplierApi } from '@/services/api/master-data'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; + +const RowOptionsMenu = ({ + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `supplier#${props.row.original.id}`; + const popoverAnchorName = `--anchor-supplier#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - - - - - - - - +
+ + + + + +
+ + + + + + + + + +
+
+
); }; @@ -93,15 +108,17 @@ const SuppliersTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '' }, + initial: { + search: '', + }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', }, }); - // Fetch Data + const [sorting, setSorting] = useState([]); + const { data: suppliers, isLoading, @@ -111,97 +128,16 @@ const SuppliersTable = () => { SupplierApi.getAllFetcher ); - // State const deleteModal = useModal(); const [selectedSupplier, setSelectedSupplier] = useState< Supplier | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - // Columns Definition - const suppliersColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'alias', - header: 'Alias', - }, - { - accessorKey: 'pic', - header: 'Nama PIC', - }, - { - accessorKey: 'category', - header: 'Kategori', - }, - { - accessorKey: 'type', - header: 'Tipe', - }, - { - accessorKey: 'phone', - header: 'No. Telp', - }, - { - accessorKey: 'email', - header: 'Email', - }, - { - accessorKey: 'address', - header: 'Alamat', - }, - { - 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 searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - const deleteClickHandler = () => { - setSelectedSupplier(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; - - // Handler const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -221,82 +157,161 @@ const SuppliersTable = () => { toast.success('Successfully delete Supplier!'); setIsDeleteLoading(false); }; - const searchChangeHandler = (e: React.ChangeEvent) => { - updateFilter('search', e.target.value); - }; - const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; - setPageSize(newVal.value as number); - }; + + const suppliersColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'alias', + header: 'Alias', + }, + { + accessorKey: 'pic', + header: 'Nama PIC', + }, + { + accessorKey: 'category', + header: 'Kategori', + }, + { + accessorKey: 'type', + header: 'Tipe', + }, + { + accessorKey: 'phone', + header: 'No. Telp', + }, + { + accessorKey: 'email', + header: 'Email', + }, + { + accessorKey: 'address', + header: 'Alamat', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 = () => { + setSelectedSupplier(props.row.original); + deleteModal.openModal(); + }; + + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(suppliers) ? suppliers?.data : []} - columns={suppliersColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(suppliers) ? suppliers?.meta?.page : 0} - totalItems={ - isResponseSuccess(suppliers) ? suppliers?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(suppliers) && suppliers?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(suppliers) || suppliers.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={suppliers?.data} + columns={suppliersColumns} + pageSize={tableFilterState.pageSize} + page={suppliers?.meta?.page ?? 0} + totalItems={suppliers?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+ []; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default SupplierTableSkeleton; diff --git a/src/components/pages/master-data/uom/UomsTable.tsx b/src/components/pages/master-data/uom/UomsTable.tsx index 51e95661..aeaae276 100644 --- a/src/components/pages/master-data/uom/UomsTable.tsx +++ b/src/components/pages/master-data/uom/UomsTable.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ChangeEventHandler, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; @@ -11,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import UomTableSkeleton from '@/components/pages/master-data/uom/skeleton/UomTableSkeleton'; import { Uom } from '@/types/api/master-data/uom'; import { UomApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `uom#${props.row.original.id}`; + const popoverAnchorName = `--anchor-uom#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -87,10 +108,17 @@ const UomsTable = () => { setPageSize, toQueryString: getTableFilterQueryString, } = useTableFilter({ - initial: { search: '', nameSort: '' }, - paramMap: { page: 'page', pageSize: 'limit', nameSort: 'sort_name' }, + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + }, }); + const [sorting, setSorting] = useState([]); + const { data: uoms, isLoading, @@ -101,65 +129,12 @@ const UomsTable = () => { ); const deleteModal = useModal(); - const [selectedUom, setSelectedUom] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const uomsColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - 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 = () => { - setSelectedUom(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -179,93 +154,130 @@ const UomsTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const uomsColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedUom(props.row.original); + deleteModal.openModal(); + }; - // 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 ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] + ); return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(uoms) ? uoms?.data : []} - columns={uomsColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(uoms) ? uoms?.meta?.page : 0} - totalItems={isResponseSuccess(uoms) ? uoms?.meta?.total_results : 0} - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': isResponseSuccess(uoms) && uoms?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(uoms) || uoms.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={uoms?.data} + columns={uomsColumns} + pageSize={tableFilterState.pageSize} + page={uoms?.meta?.page ?? 0} + totalItems={uoms?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default UomTableSkeleton; diff --git a/src/components/pages/master-data/warehouse/WarehousesTable.tsx b/src/components/pages/master-data/warehouse/WarehousesTable.tsx index 62c39574..c800e8eb 100644 --- a/src/components/pages/master-data/warehouse/WarehousesTable.tsx +++ b/src/components/pages/master-data/warehouse/WarehousesTable.tsx @@ -1,13 +1,8 @@ 'use client'; -import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; +import { ChangeEventHandler, useMemo, useState } from 'react'; import useSWR from 'swr'; -import { - CellContext, - ColumnDef, - ColumnSort, - SortingState, -} from '@tanstack/react-table'; +import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; import { Icon } from '@iconify/react'; @@ -16,71 +11,92 @@ 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 RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; +import WarehouseTableSkeleton from '@/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton'; import { Warehouse } from '@/types/api/master-data/warehouse'; import { WarehouseApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; -import { ROWS_OPTIONS } from '@/config/constant'; const RowOptionsMenu = ({ - type = 'dropdown', + popoverPosition = 'bottom', props, deleteClickHandler, }: { - type: 'dropdown' | 'collapse'; + popoverPosition: 'bottom' | 'top'; props: CellContext; deleteClickHandler: () => void; }) => { + const popoverId = `warehouse#${props.row.original.id}`; + const popoverAnchorName = `--anchor-warehouse#${props.row.original.id}`; + + const closePopover = () => { + document.getElementById(popoverId)?.hidePopover(); + }; + return ( - - - - +
+ + + - - - - - - - - + +
+ + + + + + + + + +
+
+
); }; @@ -94,23 +110,15 @@ const WarehousesTable = () => { } = useTableFilter({ initial: { search: '', - nameSort: '', - typeSort: '', - areaSort: '', - locationSort: '', - kandangSort: '', }, paramMap: { page: 'page', pageSize: 'limit', - nameSort: 'sort_name', - typeSort: 'sort_type', - areaSort: ' sort_area', - locationSort: ' sort_location', - kandangSort: ' sort_kandang', }, }); + const [sorting, setSorting] = useState([]); + const { data: warehouses, isLoading, @@ -121,101 +129,14 @@ const WarehousesTable = () => { ); const deleteModal = useModal(); - const [selectedWarehouse, setSelectedWarehouse] = useState< Warehouse | undefined >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const [sorting, setSorting] = useState([]); - - const warehousesColumns: ColumnDef[] = [ - { - header: '#', - cell: (props) => - tableFilterState.pageSize * (tableFilterState.page - 1) + - props.row.index + - 1, - }, - { - accessorKey: 'name', - header: 'Nama', - }, - { - accessorKey: 'type', - header: 'Tipe', - }, - { - accessorKey: 'area', - header: 'Area', - cell: (props) => props.row.original.area.name, - }, - { - accessorKey: 'location', - header: 'Lokasi', - cell: (props) => { - if ( - props.row.original.type === 'LOKASI' || - props.row.original.type === 'KANDANG' - ) { - return props.row.original.location.name; - } else { - return '-'; - } - }, - }, - { - accessorKey: 'kandang', - header: 'Kandang', - cell: (props) => { - if (props.row.original.type === 'KANDANG') { - return props.row.original.kandang.name; - } else { - return '-'; - } - }, - }, - { - 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 = () => { - setSelectedWarehouse(props.row.original); - deleteModal.openModal(); - }; - - return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - - ); - }, - }, - ]; + const searchChangeHandler: ChangeEventHandler = (e) => { + updateFilter('search', e.target.value); + }; const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -237,118 +158,162 @@ const WarehousesTable = () => { setIsDeleteLoading(false); }; - const searchChangeHandler: ChangeEventHandler = (e) => { - updateFilter('search', e.target.value); - }; + const warehousesColumns: ColumnDef[] = useMemo( + () => [ + { + header: 'No', + cell: (props) => + tableFilterState.pageSize * (tableFilterState.page - 1) + + props.row.index + + 1, + }, + { + accessorKey: 'name', + header: 'Nama', + }, + { + accessorKey: 'type', + header: 'Tipe', + }, + { + accessorFn: (row) => row.area?.name ?? '-', + header: 'Area', + }, + { + accessorKey: 'location', + header: 'Lokasi', + cell: (props) => { + if ( + props.row.original.type === 'LOKASI' || + props.row.original.type === 'KANDANG' + ) { + return props.row.original.location?.name ?? '-'; + } + return '-'; + }, + }, + { + accessorKey: 'kandang', + header: 'Kandang', + cell: (props) => { + if (props.row.original.type === 'KANDANG') { + return props.row.original.kandang?.name ?? '-'; + } + return '-'; + }, + }, + { + header: 'Aksi', + cell: (props: CellContext) => { + 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 pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => { - const newVal = val as OptionType; + const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - setPageSize(newVal.value as number); - }; + const deleteClickHandler = () => { + setSelectedWarehouse(props.row.original); + deleteModal.openModal(); + }; - const updateSortingFilter = useCallback( - ( - sortName: Exclude, - sortFilter: ColumnSort | undefined - ) => { - if (!sortFilter) { - updateFilter(sortName, ''); - } else { - updateFilter(sortName, sortFilter.desc ? 'desc' : 'asc'); - } - }, - [updateFilter] + return ( + + ); + }, + }, + ], + [tableFilterState.pageSize, tableFilterState.page, deleteModal] ); - // track sorting - useEffect(() => { - const nameSortFilter = sorting.find((sortItem) => sortItem.id === 'name'); - const typeSortFilter = sorting.find((sortItem) => sortItem.id === 'type'); - const areaSortFilter = sorting.find((sortItem) => sortItem.id === 'area'); - const locationSortFilter = sorting.find( - (sortItem) => sortItem.id === 'location' - ); - const kandangSortFilter = sorting.find( - (sortItem) => sortItem.id === 'kandang' - ); - - updateSortingFilter('nameSort', nameSortFilter); - updateSortingFilter('typeSort', typeSortFilter); - updateSortingFilter('areaSort', areaSortFilter); - updateSortingFilter('locationSort', locationSortFilter); - updateSortingFilter('kandangSort', kandangSortFilter); - }, [sorting, updateSortingFilter]); - return ( <> -
-
-
-
- - - -
+
+ {/* Header Section */} +
+ {/* Action Buttons */} +
+ + + +
+ {/* Search */} +
-
- -
- + } + className={{ + wrapper: 'w-full min-w-24 max-w-3xs', + inputWrapper: 'rounded-xl! shadow-button-soft', + input: + 'placeholder:font-semibold placeholder:text-base-content/50', }} - onChange={pageSizeChangeHandler} - className={{ wrapper: 'max-w-28' }} />
- - data={isResponseSuccess(warehouses) ? warehouses?.data : []} - columns={warehousesColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(warehouses) ? warehouses?.meta?.page : 0} - totalItems={ - isResponseSuccess(warehouses) ? warehouses?.meta?.total_results : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn({ - 'mb-20': - isResponseSuccess(warehouses) && warehouses?.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', - }} - /> + {/* Table Section */} +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(warehouses) || + warehouses.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={warehouses?.data} + columns={warehousesColumns} + pageSize={tableFilterState.pageSize} + page={warehouses?.meta?.page ?? 0} + totalItems={warehouses?.meta?.total_results ?? 0} + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={false} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default WarehouseTableSkeleton; diff --git a/src/components/pages/production/project-flock/ProjectFlockTable.tsx b/src/components/pages/production/project-flock/ProjectFlockTable.tsx index 3b134133..14378852 100644 --- a/src/components/pages/production/project-flock/ProjectFlockTable.tsx +++ b/src/components/pages/production/project-flock/ProjectFlockTable.tsx @@ -32,6 +32,7 @@ import StatusBadge from '@/components/helper/StatusBadge'; import PopoverButton from '@/components/popover/PopoverButton'; import PopoverContent from '@/components/popover/PopoverContent'; import ProjectFlockConfirmationModal from './ProjectFlockConfirmationModal'; +import ProjectFlockTableSkeleton from '@/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton'; import { useProjectFlockStore } from '@/stores/production/project-flock/project-flock.store'; import { ProjectFlockFormValues } from './form/ProjectFlockForm.schema'; import { useChickinStore } from '@/stores/production/chickin/chickin.store'; @@ -997,46 +998,69 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => { - - data={isResponseSuccess(projectFlocks) ? projectFlocks?.data : []} - columns={columns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(projectFlocks) ? projectFlocks?.meta?.page : 0 - } - totalItems={ - isResponseSuccess(projectFlocks) - ? projectFlocks?.meta?.total_results - : 0 - } - onPageChange={(page) => { - setPage(page); - }} - onPageSizeChange={(pageSize) => { - setPageSize(pageSize); - }} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={(row) => { - const projectFlock = row.original; - return ( - projectFlock.approval?.step_number === 1 && - projectFlock.approval?.action !== 'REJECTED' - ); - }} - withCheckbox - className={{ - containerClassName: cn('p-3', { - 'w-full mb-20': - isResponseSuccess(projectFlocks) && - projectFlocks?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(projectFlocks) || + projectFlocks.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(projectFlocks) ? projectFlocks?.data : [] + } + columns={columns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(projectFlocks) + ? projectFlocks?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(projectFlocks) + ? projectFlocks?.meta?.total_results + : 0 + } + onPageChange={(page) => { + setPage(page); + }} + onPageSizeChange={(pageSize) => { + setPageSize(pageSize); + }} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={(row) => { + const projectFlock = row.original; + return ( + projectFlock.approval?.step_number === 1 && + projectFlock.approval?.action !== 'REJECTED' + ); + }} + withCheckbox + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
diff --git a/src/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton.tsx b/src/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton.tsx new file mode 100644 index 00000000..9fe74cb8 --- /dev/null +++ b/src/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProjectFlock } from '@/types/api/production/project-flock'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProjectFlockTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no project flock data displayed. Enter project flock data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default ProjectFlockTableSkeleton; diff --git a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx index 06852fe1..0b8a299f 100644 --- a/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx +++ b/src/components/pages/production/transfer-to-laying/TransferToLayingsTable.tsx @@ -22,6 +22,7 @@ import Dropdown from '@/components/Dropdown'; import StatusBadge from '@/components/helper/StatusBadge'; import TransferToLayingFilterModal from '@/components/pages/production/transfer-to-laying/TransferToLayingFilterModal'; import TransferToLayingConfirmationModal from '@/components/pages/production/transfer-to-laying/TransferToLayingConfirmationModal'; +import TransferToLayingTableSkeleton from '@/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton'; import { TransferToLaying, @@ -596,40 +597,61 @@ const TransferToLayingsTable = () => { - - data={ - isResponseSuccess(transferToLayings) ? transferToLayings?.data : [] - } - columns={transferToLayingsColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(transferToLayings) - ? transferToLayings?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(transferToLayings) - ? transferToLayings?.meta?.total_results - : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - enableRowSelection={tableEnableRowSelectionHandler} - withCheckbox - className={{ - containerClassName: cn('p-3', { - 'w-full mb-20': - isResponseSuccess(transferToLayings) && - transferToLayings?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(transferToLayings) || + transferToLayings.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(transferToLayings) + ? transferToLayings?.data + : [] + } + columns={transferToLayingsColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(transferToLayings) + ? transferToLayings?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(transferToLayings) + ? transferToLayings?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + enableRowSelection={tableEnableRowSelectionHandler} + withCheckbox + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default TransferToLayingTableSkeleton; diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index c420ec6c..9e543902 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -53,7 +53,7 @@ import { useFormik } from 'formik'; import { UniformityTableFilterSchema, type UniformityTableFilterValues, -} from '@/components/pages/production/uniformity/UniformityTableFilter.schema'; +} from '@/components/pages/production/uniformity/filter/UniformityTableFilter'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; @@ -1010,35 +1010,55 @@ const UniformityTable = () => { - - data={isResponseSuccess(uniformities) ? uniformities?.data : []} - columns={uniformityColumns} - pageSize={tableFilterState.pageSize} - page={isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0} - totalItems={ - isResponseSuccess(uniformities) - ? uniformities?.meta?.total_results - : 0 - } - onPageChange={setPage} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - rowSelection={rowSelection} - setRowSelection={setRowSelection} - className={{ - containerClassName: cn('p-3 pt-0', { - 'mb-20': - isResponseSuccess(uniformities) && - uniformities?.data?.length === 0, - }), - headerColumnClassName: - 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', - bodyColumnClassName: - 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', - }} - emptyContent={} - /> +
+ {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(uniformities) || + uniformities.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={isResponseSuccess(uniformities) ? uniformities?.data : []} + columns={uniformityColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(uniformities) ? uniformities?.meta?.page : 0 + } + totalItems={ + isResponseSuccess(uniformities) + ? uniformities?.meta?.total_results + : 0 + } + onPageChange={setPage} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + rowSelection={rowSelection} + setRowSelection={setRowSelection} + className={{ + containerClassName: cn('p-3 pt-0 mb-0'), + headerColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + bodyColumnClassName: + 'first:pl-3 first:pr-0 xl:first:pl-3 py-3 text-nowrap', + }} + /> + )} +
{ +const UniformityTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no uniformity data displayed. Enter uniformity check data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { return ( -
- {/* Document icon */} -
-
-
- -
-
+
+
+
+
- - {/* Empty state text */} -

- No Data Available -

-

- There is no uniformity data displayed. Enter uniformity check data to - get started. -

); }; diff --git a/src/components/pages/purchase/PurchaseTable.tsx b/src/components/pages/purchase/PurchaseTable.tsx index 87992ad2..e84d56d3 100644 --- a/src/components/pages/purchase/PurchaseTable.tsx +++ b/src/components/pages/purchase/PurchaseTable.tsx @@ -17,6 +17,7 @@ import PopoverContent from '@/components/popover/PopoverContent'; import SelectInput, { OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import StatusBadge from '@/components/helper/StatusBadge'; +import PurchaseTableSkeleton from '@/components/pages/purchase/skeleton/PurchaseTableSkeleton'; import { cn, formatDate } from '@/lib/helper'; import { isResponseSuccess } from '@/lib/api-helper'; @@ -441,36 +442,55 @@ const PurchaseTable = () => { {/* Table Section */}
- - data={ - isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : [] - } - columns={purchaseColumns} - pageSize={tableFilterState.pageSize} - page={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.page - : 0 - } - totalItems={ - isResponseSuccess(purchaseRequests) - ? purchaseRequests?.meta?.total_results - : 0 - } - onPageChange={setPage} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3', { - 'w-full mb-20': - isResponseSuccess(purchaseRequests) && - purchaseRequests?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {isLoading ? ( +
+ +
+ ) : !isResponseSuccess(purchaseRequests) || + purchaseRequests.data?.length === 0 ? ( +
+ + } + /> +
+ ) : ( + + data={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.data + : [] + } + columns={purchaseColumns} + pageSize={tableFilterState.pageSize} + page={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.page + : 0 + } + totalItems={ + isResponseSuccess(purchaseRequests) + ? purchaseRequests?.meta?.total_results + : 0 + } + onPageChange={setPage} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0'), + headerColumnClassName: 'text-nowrap', + }} + /> + )}
diff --git a/src/components/pages/purchase/skeleton/PurchaseTableSkeleton.tsx b/src/components/pages/purchase/skeleton/PurchaseTableSkeleton.tsx new file mode 100644 index 00000000..16d163a1 --- /dev/null +++ b/src/components/pages/purchase/skeleton/PurchaseTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Purchase } from '@/types/api/purchase/purchase'; +import { ColumnDef } from '@tanstack/react-table'; + +const PurchaseTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no purchase data displayed. Enter purchase data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default PurchaseTableSkeleton; diff --git a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx index 0be358d8..d30e9c25 100644 --- a/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx +++ b/src/components/pages/report/marketing/tab/DailyMarketingTab.tsx @@ -36,7 +36,6 @@ import { } from '@/components/pages/report/marketing/filter/DailyMarketingFilter'; import SelectInput from '@/components/input/SelectInput'; import Modal, { useModal } from '@/components/Modal'; -import { cn } from '@/lib/helper'; import { useTabActionsStore } from '@/stores/tab-actions/tab-actions.store'; import DailyMarketingReportSkeleton from '@/components/pages/report/marketing/skeleton/DailyMarketingSkeleton'; import { useEffect as useEffectHook } from 'react'; @@ -46,8 +45,8 @@ import { MARKETING_DATE_FILTER_TYPE_OPTIONS, MARKETING_TYPE_OPTIONS, } from '@/config/constant'; -import Badge from '@/components/Badge'; import ButtonFilter from '@/components/helper/ButtonFilter'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; interface DailyMarketingTabProps { tabId: string; @@ -733,6 +732,88 @@ const DailyMarketingTab = ({ tabId }: DailyMarketingTabProps) => {
+ {/* Date Range Filter */} +
+ Tanggal +
+ { + const value = e.target.value; + formik.setFieldValue('start_date', value || null); + + if (value && formik.values.end_date) { + const startDate = new Date(value); + const endDateObj = new Date(formik.values.end_date); + + if (endDateObj < startDate) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + } else { + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + } + } else { + setHasDateError(false); + } + }} + className={{ wrapper: 'w-full' }} + errorMessage={formik.errors.start_date} + isError={ + !!formik.errors.start_date && formik.touched.start_date + } + /> +
+ { + const value = e.target.value; + formik.setFieldValue('end_date', value || null); + + if (value && formik.values.start_date) { + const startDateObj = new Date(formik.values.start_date); + const endDate = new Date(value); + + if (endDate < startDateObj) { + setHasDateError(true); + if (!dateErrorShown) { + toast.error('Tanggal akhir tidak boleh masa lampau', { + duration: Infinity, + }); + setDateErrorShown(true); + } + return; + } + } + + setHasDateError(false); + if (dateErrorShown) { + toast.dismiss(); + setDateErrorShown(false); + } + }} + className={{ wrapper: 'w-full' }} + errorMessage={formik.errors.end_date} + isError={ + (formik.errors.end_date && formik.touched.end_date) || + hasDateError + } + /> +
+
+ {/* Area Filter */} { className={{ wrapper: 'w-full' }} /> - {/* Date Range Filter */} -
- -
- { - const value = e.target.value; - formik.setFieldValue('start_date', value || null); - - if (value && formik.values.end_date) { - const startDate = new Date(value); - const endDateObj = new Date(formik.values.end_date); - - if (endDateObj < startDate) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - } else { - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - } - } else { - setHasDateError(false); - } - }} - className={{ wrapper: 'w-full' }} - errorMessage={formik.errors.start_date} - isError={ - !!formik.errors.start_date && formik.touched.start_date - } - /> - { - const value = e.target.value; - formik.setFieldValue('end_date', value || null); - - if (value && formik.values.start_date) { - const startDateObj = new Date(formik.values.start_date); - const endDate = new Date(value); - - if (endDate < startDateObj) { - setHasDateError(true); - if (!dateErrorShown) { - toast.error('Tanggal akhir tidak boleh masa lampau', { - duration: Infinity, - }); - setDateErrorShown(true); - } - return; - } - } - - setHasDateError(false); - if (dateErrorShown) { - toast.dismiss(); - setDateErrorShown(false); - } - }} - className={{ wrapper: 'w-full' }} - errorMessage={formik.errors.end_date} - isError={ - (formik.errors.end_date && formik.touched.end_date) || - hasDateError - } - /> -
-
- {/* Filter By Date Type */} - { /> {/* Marketing Type Filter */} - { label: "Ayam Afkir", value: "AYAM-AFKIR" } + */ +export function toOption(value: string): OptionType { + return { + value, + label: formatConstantLabel(value), + }; +} + +/** + * Format constant label by: + * 1. Replacing underscores/hyphens with spaces + * 2. Converting to title case + * 3. Handling special cases + */ +export function formatConstantLabel(value: string): string { + const specialCases: Record = { + 'PRE-STARTER': 'Pre Starter', + BOP: 'BOP', + SAPRONAK: 'SAPRONAK', + OVK: 'OVK', + DOC: 'DOC', + }; + + if (specialCases[value]) { + return specialCases[value]; + } + + const withSpaces = value.replace(/[-_]/g, ' '); + + return formatTitleCase(withSpaces); +} + +/** + * Transform product_flag_mapping from API format to UI format + */ +export function transformProductFlagMapping( + mapping: ConstantsApiResponse['product_flag_mapping'] +): ProductFlagMapping { + return { + flags: mapping.flags.map(toOption), + options: mapping.options.map((opt) => ({ + flag: toOption(opt.flag), + sub_flags: opt.sub_flags.map(toOption), + allow_without_sub_flag: opt.allow_without_sub_flag, + })), + sub_flag_to_flag: mapping.sub_flag_to_flag, + }; +} + +/** + * Transform approval workflows from API format to UI format + */ +export function transformApprovalWorkflows( + workflows: ConstantsApiResponse['approval_workflows'] +) { + return workflows.map((workflow) => ({ + key: workflow.key, + steps: workflow.steps.map((step) => ({ + value: String(step.step_number), + label: step.step_name, + })), + })); +} + +/** + * Transform adjustment transaction subtypes from API format to UI format + */ +export function transformAdjustmentSubtypes( + subtypes: ConstantsApiResponse['adjustment']['transaction_subtypes'] +) { + return { + RECORDING: subtypes.RECORDING.map(toOption), + PENJUALAN: subtypes.PENJUALAN.map(toOption), + PEMBELIAN: subtypes.PEMBELIAN.map(toOption), + }; +} + +/** + * Transform legacy flag aliases from API format to UI format + */ +export function transformLegacyFlagAliases( + aliases: ConstantsApiResponse['legacy_flag_aliases'] +): OptionType[] { + return Object.entries(aliases).map(([key, value]) => ({ + value: key, + label: formatConstantLabel(key), + })); +} + +/** + * Transform the entire constants API response to UI format + */ +export function transformConstants( + data: ConstantsApiResponse +): TransformedConstants { + return { + warehouse_types: data.warehouse_types.map(toOption), + supplier_categories: data.supplier_categories.map(toOption), + customer_supplier_types: data.customer_supplier_types.map(toOption), + adjustment: { + transaction_subtypes: transformAdjustmentSubtypes( + data.adjustment.transaction_subtypes + ), + }, + approval_workflows: transformApprovalWorkflows(data.approval_workflows), + flags: data.flags.map(toOption), + product_flag_mapping: transformProductFlagMapping( + data.product_flag_mapping + ), + legacy_flag_aliases: transformLegacyFlagAliases(data.legacy_flag_aliases), + stock_log: { + log_types: data.stock_log.log_types.map(toOption), + transaction_types: data.stock_log.transaction_types.map(toOption), + }, + }; +} diff --git a/src/services/api/constants/constants.ts b/src/services/api/constants/constants.ts new file mode 100644 index 00000000..0b7c2242 --- /dev/null +++ b/src/services/api/constants/constants.ts @@ -0,0 +1,25 @@ +import { httpClient } from '@/services/http/client'; +import { + ConstantsApiResponse, + TransformedConstants, +} from '@/types/api/constants/constants'; +import { transformConstants } from '@/lib/helper'; + +class ConstantsApiService { + async fetchConstants(): Promise { + try { + const response = await httpClient('/constants'); + return response; + } catch { + return undefined; + } + } + + async fetchTransformedConstants(): Promise { + const data = await this.fetchConstants(); + if (!data) return undefined; + return transformConstants(data); + } +} + +export const ConstantsApi = new ConstantsApiService(); diff --git a/src/types/api/constants/constants.d.ts b/src/types/api/constants/constants.d.ts new file mode 100644 index 00000000..00b8edbb --- /dev/null +++ b/src/types/api/constants/constants.d.ts @@ -0,0 +1,86 @@ +import { OptionType } from '@/components/input/SelectInput'; + +export type ApprovalWorkflowStep = { + step_number: number; + step_name: string; +}; + +export type ApprovalWorkflow = { + key: string; + steps: ApprovalWorkflowStep[]; +}; + +export type ProductFlagMappingOption = { + flag: string; + sub_flags: string[]; + allow_without_sub_flag: boolean; +}; + +export type ProductFlagMappingApiResponse = { + flags: string[]; + options: ProductFlagMappingOption[]; + sub_flag_to_flag: Record; +}; + +export type AdjustmentTransactionSubtypes = { + RECORDING: string[]; + PENJUALAN: string[]; + PEMBELIAN: string[]; +}; + +export type StockLogConfig = { + log_types: string[]; + transaction_types: string[]; +}; + +export type ConstantsApiResponse = { + warehouse_types: string[]; + supplier_categories: string[]; + customer_supplier_types: string[]; + adjustment: { + transaction_subtypes: AdjustmentTransactionSubtypes; + }; + approval_workflows: ApprovalWorkflow[]; + flags: string[]; + product_flag_mapping: ProductFlagMappingApiResponse; + legacy_flag_aliases: Record; + stock_log: StockLogConfig; +}; + +export type ProductFlagMappingItem = { + flag: OptionType; + sub_flags: OptionType[]; + allow_without_sub_flag: boolean; +}; + +export type ProductFlagMapping = { + flags: OptionType[]; + options: ProductFlagMappingItem[]; + sub_flag_to_flag: Record; +}; + +export type TransformedApprovalWorkflow = { + key: string; + steps: OptionType[]; +}; + +export type TransformedConstants = { + warehouse_types: OptionType[]; + supplier_categories: OptionType[]; + customer_supplier_types: OptionType[]; + adjustment: { + transaction_subtypes: { + RECORDING: OptionType[]; + PENJUALAN: OptionType[]; + PEMBELIAN: OptionType[]; + }; + }; + approval_workflows: TransformedApprovalWorkflow[]; + flags: OptionType[]; + product_flag_mapping: ProductFlagMapping; + legacy_flag_aliases: OptionType[]; + stock_log: { + log_types: OptionType[]; + transaction_types: OptionType[]; + }; +}; diff --git a/src/types/api/master-data/product.d.ts b/src/types/api/master-data/product.d.ts index c1b9b4b6..99877762 100644 --- a/src/types/api/master-data/product.d.ts +++ b/src/types/api/master-data/product.d.ts @@ -15,7 +15,10 @@ export type BaseProduct = { uom: Uom; product_category: ProductCategory; suppliers: (BaseSupplier & { price: number })[]; - flags: string[]; + flag: string; + sub_flag?: string; + sub_flags?: string[]; + flags?: string[]; }; export type Product = BaseMetadata & BaseProduct; @@ -34,7 +37,8 @@ export type CreateProductPayload = { supplier_id: number; price: number; }[]; - flags: string[]; + flag: string; + sub_flags?: string[]; }; export type UpdateProductPayload = CreateProductPayload;