From d3ce60d3baa719df839803a2eaa2c6b62486ad52 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 11:14:13 +0700 Subject: [PATCH 01/30] refactor(FE): Refactor FinanceTable and simplify Finance page structure --- src/app/finance/page.tsx | 7 +- src/components/pages/finance/FinanceTable.tsx | 704 +++++++++++------- 2 files changed, 446 insertions(+), 265 deletions(-) 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/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index f83fa469..a97d60a9 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,149 @@ 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 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'; +import SelectInputRadio from '@/components/input/SelectInputRadio'; 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 +213,9 @@ const FinanceTable = () => { }, }); + // ===== FILTER MODAL STATE ===== + const filterModal = useModal(); + // ===== State ===== const deleteModal = useModal(); const [selectedTransactionType, setSelectedTransactionType] = useState< @@ -214,6 +259,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', ''); }, }); @@ -266,10 +323,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 ) => { @@ -387,6 +475,11 @@ const FinanceTable = () => { } }; + const handleFilterModalOpen = () => { + filterModal.openModal(); + filterFormik.validateForm(); + }; + const resetFilterHandler = () => { setSelectedTransactionType(null); setSelectedBank(null); @@ -406,6 +499,7 @@ const FinanceTable = () => { updateFilter('startDate', ''); updateFilter('endDate', ''); }; + const confirmationModalDeleteClickHandler = async () => { setIsDeleteLoading(true); @@ -417,8 +511,8 @@ const FinanceTable = () => { setIsDeleteLoading(false); }; - const columns = useMemo(() => { - return [ + const columns: ColumnDef[] = useMemo( + () => [ { header: 'ID', accessorKey: 'payment_code', @@ -498,32 +592,17 @@ const FinanceTable = () => { }; return ( - <> - {currentPageSize > 2 && ( - - - - )} - - {currentPageSize <= 2 && ( - - - - )} - + ); }, }, - ]; - }, []); + ], + [] + ); useEffect(() => { return () => { @@ -555,151 +634,258 @@ 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 ? ( +
+ +
+ ) : ( + + 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: 'p-3 mb-0', + headerColumnClassName: 'text-nowrap', + }} + /> + )} +
+
+ + {/* Filter Modal */} + + {/* Modal Header */} +
+
+ +

Filter Data

+
+ +
+
+
+ {}} + closeMenuOnSelect={false} + isClearable + isMulti + className={{ wrapper: 'w-full' }} + /> + + + + + + +
+ + {/* Modal Footer */} +
+ + +
+
+
+ { onClick: confirmationModalDeleteClickHandler, }} /> -
+ ); }; From 0b01eefe2050fcc5064c68e5769ca5934ed53e44 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 11:23:06 +0700 Subject: [PATCH 02/30] refactor(FE): Update button labels for clarity in FinanceTable --- src/components/pages/finance/FinanceTable.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 01b4135a..988928e3 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -643,7 +643,7 @@ const FinanceTable = () => { className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' > - Injection Saldo Bank + Add Injection (Saldo Bank) @@ -653,7 +653,7 @@ const FinanceTable = () => { className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' > - Saldo Awal + Add Initial Balance @@ -663,7 +663,7 @@ const FinanceTable = () => { className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' > - Tambah + Add Finance @@ -761,7 +761,10 @@ const FinanceTable = () => { -
+
Date: Fri, 27 Feb 2026 11:25:37 +0700 Subject: [PATCH 03/30] refactor(FE): Refactor RowOptionsMenu to use PopoverButton and PopoverContent --- src/components/pages/finance/FinanceTable.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 988928e3..8ef90c3d 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -34,6 +34,8 @@ import { isResponseSuccess } from '@/lib/api-helper'; import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data'; import { Bank } from '@/types/api/master-data/bank'; 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 RequirePermission from '@/components/helper/RequirePermission'; @@ -64,27 +66,21 @@ const RowOptionsMenu = ({ return (
- + + -
-
+
); }; From 9f4c041ec87968fdb059d7dd4d90d665898a7d5a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 11:27:51 +0700 Subject: [PATCH 04/30] refactor(FE): Update FinanceTable layout and conditional styling --- src/components/pages/finance/FinanceTable.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 8ef90c3d..14c73b04 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -709,7 +709,7 @@ const FinanceTable = () => {
{/* Table Section */} -
+
{isLoading ? (
@@ -727,7 +727,10 @@ const FinanceTable = () => { onPageSizeChange={setPageSize} isLoading={isLoading} className={{ - containerClassName: 'p-3 mb-0', + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(finances) && finances?.data?.length === 0, + }), headerColumnClassName: 'text-nowrap', }} /> From 396a5ab5ba6209b563f2a7ad818ea6db57d198a0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 13:12:12 +0700 Subject: [PATCH 05/30] refactor(FE): Rename and update FinanceTableFilter schema import path --- src/components/pages/finance/FinanceTable.tsx | 2 +- .../{FinanceTableFilter.schema.ts => filter/FinanceFilter.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/pages/finance/{FinanceTableFilter.schema.ts => filter/FinanceFilter.ts} (100%) diff --git a/src/components/pages/finance/FinanceTable.tsx b/src/components/pages/finance/FinanceTable.tsx index 14c73b04..4d9c1a72 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -43,7 +43,7 @@ import { useUiStore } from '@/stores/ui/ui.store'; import { FinanceTableFilterSchema, FinanceTableFilterValues, -} from './FinanceTableFilter.schema'; +} from '@/components/pages/finance/filter/FinanceFilter'; const RowOptionsMenu = ({ popoverPosition = 'bottom', 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 From 64843a36aba5de7a7d2519aec8522e7a10552241 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 13:34:51 +0700 Subject: [PATCH 06/30] refactor(FE): Refactor date input components for consistency and layout --- .../expense/filter/ExpensesFilterModal.tsx | 65 +++---- src/components/pages/finance/FinanceTable.tsx | 53 +++--- .../marketing/tab/DailyMarketingTab.tsx | 174 +++++++++--------- 3 files changed, 149 insertions(+), 143 deletions(-) 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} + + )} +
{ onReset={filterFormik.handleReset} >
+
+ Tanggal +
+ +
+ +
+
+ { isClearable className={{ wrapper: 'w-full' }} /> - -
{/* Modal Footer */} 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 */} - Date: Fri, 27 Feb 2026 14:08:17 +0700 Subject: [PATCH 07/30] feat(FE): Add PRODUCT_FLAG_MAPPING with flags, sub-flags, and mappings --- src/config/constant.ts | 67 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index 12b9d89c..a754b3a9 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -383,6 +383,73 @@ export const PRODUCT_FLAG_OPTIONS = [ { label: 'VITAMIN', value: 'VITAMIN' }, ]; +export const PRODUCT_FLAG_MAPPING = { + flags: [ + { label: 'Ayam', value: 'AYAM' }, + { label: 'Pakan', value: 'PAKAN' }, + { label: 'OVK', value: 'OVK' }, + { label: 'Telur', value: 'TELUR' }, + ], + options: [ + { + flag: { label: 'Ayam', value: 'AYAM' }, + sub_flags: [ + { label: 'Ayam Afkir', value: 'AYAM-AFKIR' }, + { label: 'Ayam Culling', value: 'AYAM-CULLING' }, + { label: 'Ayam Mati', value: 'AYAM-MATI' }, + ], + allow_without_sub_flag: true, + }, + { + flag: { label: 'Pakan', value: 'PAKAN' }, + sub_flags: [ + { label: 'Pre Starter', value: 'PRE-STARTER' }, + { label: 'Starter', value: 'STARTER' }, + { label: 'Finisher', value: 'FINISHER' }, + ], + allow_without_sub_flag: false, + }, + { + flag: { label: 'OVK', value: 'OVK' }, + sub_flags: [ + { label: 'Obat', value: 'OBAT' }, + { label: 'Vitamin', value: 'VITAMIN' }, + { label: 'Kimia', value: 'KIMIA' }, + ], + allow_without_sub_flag: false, + }, + { + flag: { label: 'Telur', value: 'TELUR' }, + sub_flags: [ + { label: 'Telur Utuh', value: 'TELUR-UTUH' }, + { label: 'Telur Putih', value: 'TELUR-PUTIH' }, + { label: 'Telur Retak', value: 'TELUR-RETAK' }, + { label: 'Telur Pecah', value: 'TELUR-PECAH' }, + { label: 'Telur Papacal', value: 'TELUR-PAPACAL' }, + { label: 'Telur Jumbo', value: 'TELUR-JUMBO' }, + ], + allow_without_sub_flag: false, + }, + ], + sub_flag_to_flag: { + 'TELUR-RETAK': 'TELUR', + STARTER: 'PAKAN', + FINISHER: 'PAKAN', + VITAMIN: 'OVK', + KIMIA: 'OVK', + 'TELUR-UTUH': 'TELUR', + 'AYAM-MATI': 'AYAM', + 'PRE-STARTER': 'PAKAN', + 'TELUR-JUMBO': 'TELUR', + 'TELUR-PAPACAL': 'TELUR', + 'AYAM-AFKIR': 'AYAM', + 'AYAM-CULLING': 'AYAM', + OBAT: 'OVK', + 'TELUR-PUTIH': 'TELUR', + 'TELUR-PECAH': 'TELUR', + }, +} as const; + export const SUPPLIER_FLAG_OPTIONS = [ { label: 'EKSPEDISI', value: 'EKSPEDISI' }, ]; From a4378ebd04e3284383b805fc2fcd46600902b6dd Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 14:57:45 +0700 Subject: [PATCH 08/30] refactor(FE): Refactor product flags to support single flag and sub-flags --- .../master-data/product/ProductTable.tsx | 14 ++-- .../product/form/ProductForm.schema.ts | 13 ++-- .../master-data/product/form/ProductForm.tsx | 70 +++++++++++++++---- src/types/api/master-data/product.d.ts | 8 ++- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index 74137a14..9d0dee22 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -180,11 +180,17 @@ const ProductsTable = () => { props.row.original.suppliers?.map((s) => s.name).join(', ') || '-', }, { - accessorKey: 'flags', - header: 'Flags', + accessorKey: 'flag', + header: 'Flag', cell: (props) => - props.row.original.flags?.length - ? props.row.original.flags.join(', ') + props.row.original.flag ? props.row.original.flag : '-', + }, + { + accessorKey: 'subs_flags', + header: 'Sub Flags', + cell: (props) => + props.row.original.sub_flags?.length + ? props.row.original.sub_flags.join(', ') : '-', }, { diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 8a1d3de2..0f2a6956 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -25,7 +25,8 @@ type ProductFormSchemaType = { } | null; price: number; }[]; - flags: string[]; + flag: string; + sub_flags?: string[]; }; export const ProductFormSchema: Yup.ObjectSchema = @@ -94,10 +95,12 @@ export const ProductFormSchema: Yup.ObjectSchema = ) .required('Supplier wajib diisi!'), - flags: Yup.array() - .of(Yup.string().required()) - .min(1, 'Minimal harus ada 1 flag!') - .required('Flag wajib diisi!'), + flag: Yup.string() + .min(1, 'Flag wajib diisi!') + .required('Flag wajib diisi!') + .typeError('Flag wajib diisi!'), + + sub_flags: Yup.array().of(Yup.string().required()), }); 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..0a2887be 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -36,8 +36,10 @@ 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 { 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'; @@ -110,7 +112,8 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { price: supplier.price, })) : [], - flags: initialValues?.flags ?? [], + flag: initialValues?.flag ?? '', + sub_flags: initialValues?.sub_flags ?? [], }), [initialValues] ); @@ -139,7 +142,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 +204,28 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { }); }, [supplierOptions, formik.values.suppliers]); + const selectedFlagMapping = useMemo(() => { + return PRODUCT_FLAG_MAPPING.options.find( + (opt) => opt.flag.value === formik.values.flag + ); + }, [formik.values.flag]); + + const subFlagOptions = useMemo(() => { + return (selectedFlagMapping?.sub_flags as unknown as OptionType[]) ?? []; + }, [selectedFlagMapping]); + + const selectedSubFlagValues = useMemo(() => { + return ( + (selectedFlagMapping?.sub_flags.filter((subFlag) => + formik.values.sub_flags?.includes(subFlag.value) + ) as unknown as OptionType[]) ?? [] + ); + }, [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 +239,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 +453,45 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { readOnly={type === 'detail'} />
-
+
opt.value === formik.values.flag)} + onChange={(val) => { + const selectedFlag = (val as OptionType)?.value ?? ''; + formik.setFieldValue('flag', selectedFlag); + formik.setFieldValue('sub_flags', []); + }} + options={PRODUCT_FLAG_MAPPING.flags as unknown as OptionType[]} + isError={formik.touched.flag && Boolean(formik.errors.flag)} + errorMessage={formik.errors.flag as string} + isDisabled={type === 'detail'} + isClearable + /> + + - (formik.values.flags || []).includes(opt.value) - )} + required={isSubFlagRequired} + value={selectedSubFlagValues} onChange={(val) => { const arr = Array.isArray(val) ? val : val ? [val] : []; formik.setFieldValue( - 'flags', + 'sub_flags', arr.map((v) => (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} + isError={ + formik.touched.sub_flags && Boolean(formik.errors.sub_flags) + } + errorMessage={formik.errors.sub_flags as string} + isDisabled={type === 'detail' || !formik.values.flag} isClearable />
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; From d1c6fe8fb44e3621c7e008b51a946bd7186d6567 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 15:03:09 +0700 Subject: [PATCH 09/30] refactor(FE): Add conditional validation for sub_flags in ProductForm.schema --- .../product/form/ProductForm.schema.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.schema.ts b/src/components/pages/master-data/product/form/ProductForm.schema.ts index 0f2a6956..e0d4f37f 100644 --- a/src/components/pages/master-data/product/form/ProductForm.schema.ts +++ b/src/components/pages/master-data/product/form/ProductForm.schema.ts @@ -1,4 +1,5 @@ import * as Yup from 'yup'; +import { PRODUCT_FLAG_MAPPING } from '@/config/constant'; type ProductFormSchemaType = { name: string; @@ -100,7 +101,21 @@ export const ProductFormSchema: Yup.ObjectSchema = .required('Flag wajib diisi!') .typeError('Flag wajib diisi!'), - sub_flags: Yup.array().of(Yup.string().required()), + sub_flags: Yup.array() + .of(Yup.string().required()) + .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; From 9bd4a73a904a15c3da2c506614d3a82c1bb097a3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 15:36:44 +0700 Subject: [PATCH 10/30] refactor(FE): Refactor flags field to boolean and update form handling --- .../nonstock/form/NonstockForm.schema.ts | 2 +- .../nonstock/form/NonstockForm.tsx | 37 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts index 8039ef76..c427133c 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.schema.ts @@ -17,7 +17,7 @@ export const NonstockFormSchema = Yup.object({ }) ), - flags: Yup.array().of(Yup.string()).notRequired(), + flags: Yup.boolean().default(false), }); export const UpdateNonstockFormSchema = NonstockFormSchema; diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 883aac03..7fdcc564 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -12,6 +12,7 @@ import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; +import { RadioGroup } from '@/components/input/RadioInput'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import RequirePermission from '@/components/helper/RequirePermission'; @@ -29,8 +30,7 @@ import { } from '@/types/api/master-data/nonstock'; import { NonstockApi, SupplierApi, UomApi } from '@/services/api/master-data'; import { cn } from '@/lib/helper'; -import { flags } from '@/types/api/api-general'; -import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; + import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import AlertErrorList from '@/components/helper/form/FormErrors'; import { Supplier } from '@/types/api/master-data/supplier'; @@ -97,7 +97,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { 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('isExpedition', 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} +
From 80a94c48c3cfada2a4e541f042d71010aeb66df0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 15:38:11 +0700 Subject: [PATCH 11/30] refactor(FE): Format product prices using `formatCurrency` helper --- .../pages/master-data/product/ProductTable.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index 9d0dee22..c265be55 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -24,7 +24,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { Product } from '@/types/api/master-data/product'; import { ProductApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; +import { cn, formatCurrency } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { ROWS_OPTIONS } from '@/config/constant'; @@ -155,13 +155,17 @@ const ProductsTable = () => { accessorKey: 'product_price', header: 'Harga Produk', cell: (props) => - props.row.original.product_price?.toLocaleString() ?? '-', + props.row.original.product_price + ? formatCurrency(props.row.original.product_price) + : '-', }, { accessorKey: 'selling_price', header: 'Harga Jual', cell: (props) => - props.row.original.selling_price?.toLocaleString() ?? '-', + props.row.original.selling_price + ? formatCurrency(props.row.original.selling_price) + : '-', }, { accessorKey: 'tax', From 3aec4125991f207ecb6d8c54d5255d34245a55aa Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 27 Feb 2026 15:38:47 +0700 Subject: [PATCH 12/30] refactor(FE): Update label text in NonstockForm RadioGroup --- src/components/pages/master-data/nonstock/form/NonstockForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index 7fdcc564..c8bc7ece 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -265,7 +265,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { /> Date: Fri, 27 Feb 2026 15:42:24 +0700 Subject: [PATCH 13/30] refactor(FE): Update label in NonstockForm to 'Flags Ekspedisi' --- src/components/pages/master-data/nonstock/form/NonstockForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index c8bc7ece..ccf0ecbe 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -265,7 +265,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { /> Date: Mon, 2 Mar 2026 09:57:21 +0700 Subject: [PATCH 14/30] feat(FE): Add constants transformation utilities and API service --- src/lib/helper.ts | 126 ++++++++++++++++++++++++ src/services/api/constants/constants.ts | 25 +++++ src/types/api/constants/constants.d.ts | 86 ++++++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 src/services/api/constants/constants.ts create mode 100644 src/types/api/constants/constants.d.ts diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 9a802f80..9029321a 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -3,6 +3,12 @@ import 'moment/locale/id'; import { twMerge } from 'tailwind-merge'; import clsx, { ClassValue } from 'clsx'; import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; +import { OptionType } from '@/components/input/SelectInput'; +import { + ConstantsApiResponse, + ProductFlagMappingUI, + TransformedConstants, +} from '@/types/api/constants/constants'; // set locale globally moment.locale('id'); @@ -179,3 +185,123 @@ export function findMenuPath( return null; } + +/** + * Transform a string value to OptionType with formatted label + * Example: "AYAM-AFKIR" -> { 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'] +): ProductFlagMappingUI { + 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..e2a7c670 --- /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 ProductFlagMappingOptionUI = { + flag: OptionType; + sub_flags: OptionType[]; + allow_without_sub_flag: boolean; +}; + +export type ProductFlagMappingUI = { + flags: OptionType[]; + options: ProductFlagMappingOptionUI[]; + sub_flag_to_flag: Record; +}; + +export type ApprovalWorkflowUI = { + 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: ApprovalWorkflowUI[]; + flags: OptionType[]; + product_flag_mapping: ProductFlagMappingUI; + legacy_flag_aliases: OptionType[]; + stock_log: { + log_types: OptionType[]; + transaction_types: OptionType[]; + }; +}; From 4e3c6736ab1b05e2b0ce8f136637d1d6b13a9550 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 09:58:01 +0700 Subject: [PATCH 15/30] refactor(FE): Rename "Sub Flags" header to "Kategori Flags" --- src/components/pages/master-data/product/ProductTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index c265be55..f2398065 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -191,7 +191,7 @@ const ProductsTable = () => { }, { accessorKey: 'subs_flags', - header: 'Sub Flags', + header: 'Kategori Flags', cell: (props) => props.row.original.sub_flags?.length ? props.row.original.sub_flags.join(', ') From ef9d820c0df52736eb62e1e57f2a7a3ad31ff5fc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 10:06:06 +0700 Subject: [PATCH 16/30] refactor(FE): Refactor ProductForm to fetch constants via SWR --- .../master-data/product/form/ProductForm.tsx | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 0a2887be..e3431453 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -38,7 +38,9 @@ import { import { cn } from '@/lib/helper'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; -import { PRODUCT_FLAG_MAPPING } from '@/config/constant'; +import { ConstantsApi } from '@/services/api/constants/constants'; +import type { TransformedConstants } from '@/types/api/constants/constants'; +import useSWR from 'swr'; import { Supplier } from '@/types/api/master-data/supplier'; import Card from '@/components/Card'; @@ -55,6 +57,11 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const { data: constants, isLoading: isLoadingConstants } = useSWR< + TransformedConstants | undefined + >('constants', ConstantsApi.fetchTransformedConstants.bind(ConstantsApi)); + + const productFlagMapping = constants?.product_flag_mapping ?? null; const createProductHandler = useCallback( async (payload: CreateProductPayload) => { @@ -205,20 +212,20 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { }, [supplierOptions, formik.values.suppliers]); const selectedFlagMapping = useMemo(() => { - return PRODUCT_FLAG_MAPPING.options.find( + return productFlagMapping?.options.find( (opt) => opt.flag.value === formik.values.flag ); - }, [formik.values.flag]); + }, [formik.values.flag, productFlagMapping]); const subFlagOptions = useMemo(() => { - return (selectedFlagMapping?.sub_flags as unknown as OptionType[]) ?? []; + return selectedFlagMapping?.sub_flags ?? []; }, [selectedFlagMapping]); const selectedSubFlagValues = useMemo(() => { return ( - (selectedFlagMapping?.sub_flags.filter((subFlag) => - formik.values.sub_flags?.includes(subFlag.value) - ) as unknown as OptionType[]) ?? [] + selectedFlagMapping?.sub_flags.filter((subFlag) => + formik.values.sub_flags?.includes(subFlag.value as string) + ) ?? [] ); }, [selectedFlagMapping, formik.values.sub_flags]); @@ -458,15 +465,16 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { required label='Flags' placeholder='Pilih flags...' - value={( - PRODUCT_FLAG_MAPPING.flags as unknown as OptionType[] - ).find((opt) => opt.value === formik.values.flag)} + value={productFlagMapping?.flags.find( + (opt) => opt.value === formik.values.flag + )} onChange={(val) => { - const selectedFlag = (val as OptionType)?.value ?? ''; + const selectedFlag = String((val as OptionType)?.value ?? ''); formik.setFieldValue('flag', selectedFlag); formik.setFieldValue('sub_flags', []); }} - options={PRODUCT_FLAG_MAPPING.flags as unknown as OptionType[]} + options={productFlagMapping?.flags ?? []} + isLoading={isLoadingConstants} isError={formik.touched.flag && Boolean(formik.errors.flag)} errorMessage={formik.errors.flag as string} isDisabled={type === 'detail'} @@ -474,8 +482,8 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { /> { const arr = Array.isArray(val) ? val : val ? [val] : []; formik.setFieldValue( 'sub_flags', - arr.map((v) => (v as OptionType).value) + arr.map((v) => String((v as OptionType).value)) ); }} options={subFlagOptions} + isLoading={isLoadingConstants} 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 />
From 56d4b8a5c9611d17f31f442fd9636be819275b5b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 10:15:06 +0700 Subject: [PATCH 17/30] refactor(FE): Update ProductFlagMapping types and related references --- src/lib/helper.ts | 4 ++-- src/types/api/constants/constants.d.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 9029321a..383f35c3 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -6,7 +6,7 @@ import { SidebarMenuItem } from '@/components/molecules/SidebarMenu'; import { OptionType } from '@/components/input/SelectInput'; import { ConstantsApiResponse, - ProductFlagMappingUI, + ProductFlagMapping, TransformedConstants, } from '@/types/api/constants/constants'; @@ -226,7 +226,7 @@ export function formatConstantLabel(value: string): string { */ export function transformProductFlagMapping( mapping: ConstantsApiResponse['product_flag_mapping'] -): ProductFlagMappingUI { +): ProductFlagMapping { return { flags: mapping.flags.map(toOption), options: mapping.options.map((opt) => ({ diff --git a/src/types/api/constants/constants.d.ts b/src/types/api/constants/constants.d.ts index e2a7c670..00b8edbb 100644 --- a/src/types/api/constants/constants.d.ts +++ b/src/types/api/constants/constants.d.ts @@ -47,19 +47,19 @@ export type ConstantsApiResponse = { stock_log: StockLogConfig; }; -export type ProductFlagMappingOptionUI = { +export type ProductFlagMappingItem = { flag: OptionType; sub_flags: OptionType[]; allow_without_sub_flag: boolean; }; -export type ProductFlagMappingUI = { +export type ProductFlagMapping = { flags: OptionType[]; - options: ProductFlagMappingOptionUI[]; + options: ProductFlagMappingItem[]; sub_flag_to_flag: Record; }; -export type ApprovalWorkflowUI = { +export type TransformedApprovalWorkflow = { key: string; steps: OptionType[]; }; @@ -75,9 +75,9 @@ export type TransformedConstants = { PEMBELIAN: OptionType[]; }; }; - approval_workflows: ApprovalWorkflowUI[]; + approval_workflows: TransformedApprovalWorkflow[]; flags: OptionType[]; - product_flag_mapping: ProductFlagMappingUI; + product_flag_mapping: ProductFlagMapping; legacy_flag_aliases: OptionType[]; stock_log: { log_types: OptionType[]; From 8397d76171555b5e245d12af711dca3f5cb31fb3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 10:15:53 +0700 Subject: [PATCH 18/30] refactor(FE): Add fallback for product flag mapping on constants error --- .../master-data/product/form/ProductForm.tsx | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index e3431453..aa5217c5 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -39,8 +39,12 @@ import { cn } from '@/lib/helper'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { ConstantsApi } from '@/services/api/constants/constants'; -import type { TransformedConstants } from '@/types/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'; @@ -57,11 +61,24 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { const [productFormErrorMessage, setProductFormErrorMessage] = useState(''); const [isDeleteLoading, setIsDeleteLoading] = useState(false); - const { data: constants, isLoading: isLoadingConstants } = useSWR< - TransformedConstants | undefined - >('constants', ConstantsApi.fetchTransformedConstants.bind(ConstantsApi)); + const { + data: constants, + error: constantsError, + isLoading: isLoadingConstants, + } = useSWR( + 'constants', + ConstantsApi.fetchTransformedConstants.bind(ConstantsApi), + { + shouldRetryOnError: false, + } + ); - const productFlagMapping = constants?.product_flag_mapping ?? null; + const productFlagMapping: ProductFlagMapping | null = useMemo(() => { + if (constantsError || !constants) { + return PRODUCT_FLAG_MAPPING as unknown as ProductFlagMapping; + } + return constants.product_flag_mapping; + }, [constants, constantsError]); const createProductHandler = useCallback( async (payload: CreateProductPayload) => { From 7eaf6b7a3adb1b31f69945e5e0a4460ffb2b7af9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 10:42:34 +0700 Subject: [PATCH 19/30] refactor(FE): Refactor ProductTable and RowOptionsMenu for improved clarity --- src/app/master-data/product/page.tsx | 6 +- .../master-data/product/ProductTable.tsx | 499 ++++++++---------- 2 files changed, 233 insertions(+), 272 deletions(-) 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/components/pages/master-data/product/ProductTable.tsx b/src/components/pages/master-data/product/ProductTable.tsx index f2398065..9953158d 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,94 @@ 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 { Product } from '@/types/api/master-data/product'; import { ProductApi } from '@/services/api/master-data'; import { cn, 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 +110,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,124 +133,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 - ? 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 ?? '-', - }, - { - accessorKey: 'suppliers', - header: 'Supplier', - cell: (props) => - props.row.original.suppliers?.map((s) => s.name).join(', ') || '-', - }, - { - accessorKey: 'flag', - header: 'Flag', - cell: (props) => - props.row.original.flag ? props.row.original.flag : '-', - }, - { - accessorKey: 'subs_flags', - header: 'Kategori Flags', - cell: (props) => - props.row.original.sub_flags?.length - ? props.row.original.sub_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); @@ -258,110 +158,175 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(products) && products?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
+ Date: Mon, 2 Mar 2026 10:47:17 +0700 Subject: [PATCH 20/30] refactor(FE): Refactor NonstocksTable to use Popover for row actions --- .../master-data/nonstock/NonstocksTable.tsx | 409 ++++++++---------- 1 file changed, 184 insertions(+), 225 deletions(-) diff --git a/src/components/pages/master-data/nonstock/NonstocksTable.tsx b/src/components/pages/master-data/nonstock/NonstocksTable.tsx index 6aeb3f99..1a4cb358 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 { 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,128 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(nonstocks) && nonstocks?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
Date: Mon, 2 Mar 2026 11:17:54 +0700 Subject: [PATCH 21/30] refactor(FE): Refactor master-data pages to simplify component rendering --- src/app/master-data/area/page.tsx | 6 +- src/app/master-data/bank/page.tsx | 6 +- src/app/master-data/customer/page.tsx | 6 +- src/app/master-data/flock/page.tsx | 6 +- src/app/master-data/kandang/page.tsx | 6 +- src/app/master-data/location/page.tsx | 6 +- src/app/master-data/nonstock/page.tsx | 6 +- src/app/master-data/product-category/page.tsx | 6 +- .../master-data/production-standard/page.tsx | 6 +- src/app/master-data/supplier/page.tsx | 6 +- src/app/master-data/uom/page.tsx | 6 +- src/app/master-data/warehouse/page.tsx | 6 +- .../pages/master-data/area/AreasTable.tsx | 354 +++++++------- .../pages/master-data/bank/BanksTable.tsx | 378 ++++++++------- .../master-data/customer/CustomersTable.tsx | 408 ++++++++-------- .../pages/master-data/flock/FlocksTable.tsx | 398 +++++++-------- .../master-data/kandang/KandangsTable.tsx | 406 +++++++--------- .../master-data/location/LocationsTable.tsx | 394 +++++++-------- .../master-data/nonstock/NonstocksTable.tsx | 2 +- .../product-category/ProductCategoryTable.tsx | 395 +++++++-------- .../master-data/product/ProductTable.tsx | 2 +- .../ProductionStandardTable.tsx | 338 +++++++------ .../master-data/supplier/SupplierTable.tsx | 438 ++++++++--------- .../pages/master-data/uom/UomsTable.tsx | 350 +++++++------- .../master-data/warehouse/WarehousesTable.tsx | 452 ++++++++---------- 25 files changed, 2118 insertions(+), 2269 deletions(-) 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/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/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index d92c7840..cc8084d8 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 RequirePermission from '@/components/helper/RequirePermission'; +import PopoverButton from '@/components/popover/PopoverButton'; +import PopoverContent from '@/components/popover/PopoverContent'; 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,114 @@ 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 */} +
+ + 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: cn('p-3 mb-0', { + 'w-full': isResponseSuccess(areas) && areas?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
; 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,127 @@ 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 */} +
+ + 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: cn('p-3 mb-0', { + 'w-full': isResponseSuccess(banks) && banks?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
; 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,132 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(customers) && customers?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
; 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,128 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(flocks) && flocks?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
+ ; 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 +110,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 +129,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 +158,128 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(kandangs) && kandangs?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
; 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,123 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(locations) && locations?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
{ className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' > - Tambah Nonstock + Add Nonstock
diff --git a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx index 11199c73..3a872b7f 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,11 +17,9 @@ 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 { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategoryApi } from '@/services/api/master-data'; @@ -23,60 +27,83 @@ 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,86 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(productCategories) && + productCategories?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
+ { className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm' > - Tambah Produk + Add Product
diff --git a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx index a8df6ae8..09e83fd1 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 { 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 { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import toast from 'react-hot-toast'; -import { cn } from '@/lib/helper'; + +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 { ProductionStandard } from '@/types/api/master-data/production-standard'; +import { ProductionStandardApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; 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,107 @@ 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 */} +
+ + + +
- + + {/* Table Section */} +
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 && ( - - - - )} - - ); - }, - }, - ]} + columns={productionStandardColumns} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} 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' - ), + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(productionStandards) && + productionStandards?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', }} /> - +
- - - + + ); }; diff --git a/src/components/pages/master-data/supplier/SupplierTable.tsx b/src/components/pages/master-data/supplier/SupplierTable.tsx index 2620c9e6..e5b225ec 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'; -const RowOptions = ({ - type = 'dropdown', +import { Supplier } from '@/types/api/master-data/supplier'; +import { SupplierApi } from '@/services/api/master-data'; +import { cn } from '@/lib/helper'; +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,146 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(suppliers) && suppliers?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
+ ; 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,112 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': isResponseSuccess(uoms) && uoms?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
; 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,149 @@ 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 */} +
+ + 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} + onPageSizeChange={setPageSize} + isLoading={isLoading} + sorting={sorting} + setSorting={setSorting} + className={{ + containerClassName: cn('p-3 mb-0', { + 'w-full': + isResponseSuccess(warehouses) && + warehouses?.data?.length === 0, + }), + headerColumnClassName: 'text-nowrap', + }} + /> +
Date: Mon, 2 Mar 2026 11:34:27 +0700 Subject: [PATCH 22/30] refactor(FE): Refactor UniformityTable to improve loading and empty states --- .../production/uniformity/UniformityTable.tsx | 78 ++++++++++++------- .../skeleton/UniformityTableSkeleton.tsx | 53 +++++++------ 2 files changed, 78 insertions(+), 53 deletions(-) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index c420ec6c..71bd3901 100644 --- a/src/components/pages/production/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -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. -

); }; From d96388e5f4a94e9b87c8cc82ca6e388f928011c1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 11:42:34 +0700 Subject: [PATCH 23/30] refactor(FE): Rename and update import for UniformityTableFilter module --- src/components/pages/production/uniformity/UniformityTable.tsx | 2 +- .../UniformityTableFilter.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/pages/production/uniformity/{UniformityTableFilter.schema.ts => filter/UniformityTableFilter.ts} (100%) diff --git a/src/components/pages/production/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx index 71bd3901..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'; diff --git a/src/components/pages/production/uniformity/UniformityTableFilter.schema.ts b/src/components/pages/production/uniformity/filter/UniformityTableFilter.ts similarity index 100% rename from src/components/pages/production/uniformity/UniformityTableFilter.schema.ts rename to src/components/pages/production/uniformity/filter/UniformityTableFilter.ts From d3501e5f3db5bd6d3701220cb583af6efa182832 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 11:56:27 +0700 Subject: [PATCH 24/30] refactor(FE): Add skeleton loaders for ProjectFlock and TransferToLaying tables --- .../project-flock/ProjectFlockTable.tsx | 104 +++++++++++------- .../skeleton/ProjectFlockTableSkeleton.tsx | 37 +++++++ .../TransferToLayingsTable.tsx | 90 +++++++++------ .../TransferToLayingTableSkeleton.tsx | 37 +++++++ 4 files changed, 194 insertions(+), 74 deletions(-) create mode 100644 src/components/pages/production/project-flock/skeleton/ProjectFlockTableSkeleton.tsx create mode 100644 src/components/pages/production/transfer-to-laying/skeleton/TransferToLayingTableSkeleton.tsx 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; From 9c4c750664dac01d5c767c08a1fc3048739dd3f0 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 12:10:06 +0700 Subject: [PATCH 25/30] refactor(FE): Refactor table skeleton components for consistency --- .../pages/closing/ClosingsTable.tsx | 31 ++++--- .../closing/skeleton/ClosingTableSkeleton.tsx | 8 +- .../pages/expense/ExpensesTable.tsx | 66 +++++++++------ .../expense/skeleton/ExpenseTableSkeleton.tsx | 37 +++++++++ src/components/pages/finance/FinanceTable.tsx | 20 ++++- .../finance/skeleton/FinanceTableSkeleton.tsx | 37 +++++++++ .../pages/marketing/MarketingTable.tsx | 66 ++++++++++----- .../skeleton/MarketingTableSkeleton.tsx | 37 +++++++++ .../pages/purchase/PurchaseTable.tsx | 80 ++++++++++++------- .../skeleton/PurchaseTableSkeleton.tsx | 37 +++++++++ 10 files changed, 318 insertions(+), 101 deletions(-) create mode 100644 src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx create mode 100644 src/components/pages/finance/skeleton/FinanceTableSkeleton.tsx create mode 100644 src/components/pages/marketing/skeleton/MarketingTableSkeleton.tsx create mode 100644 src/components/pages/purchase/skeleton/PurchaseTableSkeleton.tsx 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/skeleton/ExpenseTableSkeleton.tsx b/src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx new file mode 100644 index 00000000..f159a81a --- /dev/null +++ b/src/components/pages/expense/skeleton/ExpenseTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Expense } from '@/types/api/expense'; +import { ColumnDef } from '@tanstack/react-table'; + +const ExpenseTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no expense data displayed. Enter expense data to get started.', +}: { + columns: ColumnDef[]; + 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 f8eacd77..100eecb4 100644 --- a/src/components/pages/finance/FinanceTable.tsx +++ b/src/components/pages/finance/FinanceTable.tsx @@ -44,6 +44,7 @@ import { FinanceTableFilterSchema, FinanceTableFilterValues, } from '@/components/pages/finance/filter/FinanceFilter'; +import FinanceTableSkeleton from '@/components/pages/finance/skeleton/FinanceTableSkeleton'; const RowOptionsMenu = ({ popoverPosition = 'bottom', @@ -714,6 +715,20 @@ const FinanceTable = () => {
+ ) : !isResponseSuccess(finances) || finances.data?.length === 0 ? ( +
+ + } + /> +
) : ( data={isResponseSuccess(finances) ? finances.data : []} @@ -727,10 +742,7 @@ const FinanceTable = () => { onPageSizeChange={setPageSize} isLoading={isLoading} className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(finances) && finances?.data?.length === 0, - }), + containerClassName: cn('p-3 mb-0'), headerColumnClassName: 'text-nowrap', }} /> 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/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/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; From 1341b1ff532e59f64394bf1516bd5dd48925a921 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 13:31:23 +0700 Subject: [PATCH 26/30] refactor(FE): Add skeleton loaders for inventory tables --- .../adjustment/InventoryAdjustmentTable.tsx | 82 +++++++++++-------- .../InventoryAdjustmentTableSkeleton.tsx | 37 +++++++++ .../inventory/movement/MovementTable.tsx | 60 +++++++++----- .../skeleton/MovementTableSkeleton.tsx | 37 +++++++++ .../product/InventoryProductTable.tsx | 80 +++++++++++------- .../InventoryProductTableSkeleton.tsx | 37 +++++++++ 6 files changed, 250 insertions(+), 83 deletions(-) create mode 100644 src/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton.tsx create mode 100644 src/components/pages/inventory/movement/skeleton/MovementTableSkeleton.tsx create mode 100644 src/components/pages/inventory/product/skeleton/InventoryProductTableSkeleton.tsx 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; From f2b59ded3cc6ac545b48073562dc03f536fdf32c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 13:50:25 +0700 Subject: [PATCH 27/30] feat(FE): Add skeleton components for master data tables --- .../pages/master-data/area/AreasTable.tsx | 62 ++++++++++------ .../area/skeleton/AreaTableSkeleton.tsx | 37 ++++++++++ .../pages/master-data/bank/BanksTable.tsx | 60 ++++++++++------ .../bank/skeleton/BankTableSkeleton.tsx | 37 ++++++++++ .../master-data/customer/CustomersTable.tsx | 59 +++++++++------ .../skeleton/CustomerTableSkeleton.tsx | 37 ++++++++++ .../pages/master-data/flock/FlocksTable.tsx | 59 +++++++++------ .../flock/skeleton/FlockTableSkeleton.tsx | 37 ++++++++++ .../master-data/kandang/KandangsTable.tsx | 60 ++++++++++------ .../kandang/skeleton/KandangTableSkeleton.tsx | 37 ++++++++++ .../master-data/location/LocationsTable.tsx | 59 +++++++++------ .../skeleton/LocationTableSkeleton.tsx | 37 ++++++++++ .../master-data/nonstock/NonstocksTable.tsx | 59 +++++++++------ .../skeleton/NonstockTableSkeleton.tsx | 37 ++++++++++ .../product-category/ProductCategoryTable.tsx | 71 ++++++++++--------- .../skeleton/ProductCategoryTableSkeleton.tsx | 37 ++++++++++ .../master-data/product/ProductTable.tsx | 60 ++++++++++------ .../product/skeleton/ProductTableSkeleton.tsx | 37 ++++++++++ .../ProductionStandardTable.tsx | 53 ++++++++------ .../ProductionStandardTableSkeleton.tsx | 37 ++++++++++ .../master-data/supplier/SupplierTable.tsx | 59 +++++++++------ .../skeleton/SupplierTableSkeleton.tsx | 37 ++++++++++ .../pages/master-data/uom/UomsTable.tsx | 56 ++++++++++----- .../uom/skeleton/UomTableSkeleton.tsx | 37 ++++++++++ .../master-data/warehouse/WarehousesTable.tsx | 63 +++++++++------- .../skeleton/WarehouseTableSkeleton.tsx | 37 ++++++++++ 26 files changed, 967 insertions(+), 294 deletions(-) create mode 100644 src/components/pages/master-data/area/skeleton/AreaTableSkeleton.tsx create mode 100644 src/components/pages/master-data/bank/skeleton/BankTableSkeleton.tsx create mode 100644 src/components/pages/master-data/customer/skeleton/CustomerTableSkeleton.tsx create mode 100644 src/components/pages/master-data/flock/skeleton/FlockTableSkeleton.tsx create mode 100644 src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx create mode 100644 src/components/pages/master-data/location/skeleton/LocationTableSkeleton.tsx create mode 100644 src/components/pages/master-data/nonstock/skeleton/NonstockTableSkeleton.tsx create mode 100644 src/components/pages/master-data/product-category/skeleton/ProductCategoryTableSkeleton.tsx create mode 100644 src/components/pages/master-data/product/skeleton/ProductTableSkeleton.tsx create mode 100644 src/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton.tsx create mode 100644 src/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton.tsx create mode 100644 src/components/pages/master-data/uom/skeleton/UomTableSkeleton.tsx create mode 100644 src/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton.tsx diff --git a/src/components/pages/master-data/area/AreasTable.tsx b/src/components/pages/master-data/area/AreasTable.tsx index cc8084d8..1884dca3 100644 --- a/src/components/pages/master-data/area/AreasTable.tsx +++ b/src/components/pages/master-data/area/AreasTable.tsx @@ -11,13 +11,13 @@ 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 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'; @@ -241,26 +241,44 @@ const AreasTable = () => { {/* Table Section */}
- - 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: cn('p-3 mb-0', { - 'w-full': isResponseSuccess(areas) && areas?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/area/skeleton/AreaTableSkeleton.tsx b/src/components/pages/master-data/area/skeleton/AreaTableSkeleton.tsx new file mode 100644 index 00000000..14e35fb9 --- /dev/null +++ b/src/components/pages/master-data/area/skeleton/AreaTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Area } from '@/types/api/master-data/area'; +import { ColumnDef } from '@tanstack/react-table'; + +const AreaTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no area data displayed. Enter area data to get started.', +}: { + columns: ColumnDef[]; + 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 010d6dec..a6a5dbef 100644 --- a/src/components/pages/master-data/bank/BanksTable.tsx +++ b/src/components/pages/master-data/bank/BanksTable.tsx @@ -14,10 +14,10 @@ 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 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'; @@ -254,26 +254,44 @@ const BanksTable = () => { {/* Table Section */}
- - 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: cn('p-3 mb-0', { - 'w-full': isResponseSuccess(banks) && banks?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/bank/skeleton/BankTableSkeleton.tsx b/src/components/pages/master-data/bank/skeleton/BankTableSkeleton.tsx new file mode 100644 index 00000000..7a3a2b38 --- /dev/null +++ b/src/components/pages/master-data/bank/skeleton/BankTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Bank } from '@/types/api/master-data/bank'; +import { ColumnDef } from '@tanstack/react-table'; + +const BankTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no bank data displayed. Enter bank data to get started.', +}: { + columns: ColumnDef[]; + 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 58ba6d40..2768daa3 100644 --- a/src/components/pages/master-data/customer/CustomersTable.tsx +++ b/src/components/pages/master-data/customer/CustomersTable.tsx @@ -14,10 +14,10 @@ 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 CustomerTableSkeleton from '@/components/pages/master-data/customer/skeleton/CustomerTableSkeleton'; import { Customer } from '@/types/api/master-data/customer'; import { CustomerApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -261,27 +261,42 @@ const CustomersTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(customers) && customers?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/customer/skeleton/CustomerTableSkeleton.tsx b/src/components/pages/master-data/customer/skeleton/CustomerTableSkeleton.tsx new file mode 100644 index 00000000..f498cc60 --- /dev/null +++ b/src/components/pages/master-data/customer/skeleton/CustomerTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Customer } from '@/types/api/master-data/customer'; +import { ColumnDef } from '@tanstack/react-table'; + +const CustomerTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no customer data displayed. Enter customer data to get started.', +}: { + columns: ColumnDef[]; + 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 f7156d55..3550a346 100644 --- a/src/components/pages/master-data/flock/FlocksTable.tsx +++ b/src/components/pages/master-data/flock/FlocksTable.tsx @@ -14,10 +14,10 @@ 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 { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -249,27 +249,42 @@ const FlockTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(flocks) && flocks?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/flock/skeleton/FlockTableSkeleton.tsx b/src/components/pages/master-data/flock/skeleton/FlockTableSkeleton.tsx new file mode 100644 index 00000000..6a5c034f --- /dev/null +++ b/src/components/pages/master-data/flock/skeleton/FlockTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Flock } from '@/types/api/master-data/flock'; +import { ColumnDef } from '@tanstack/react-table'; + +const FlockTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no flock data displayed. Enter flock data to get started.', +}: { + columns: ColumnDef[]; + 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 55604939..63e9fa58 100644 --- a/src/components/pages/master-data/kandang/KandangsTable.tsx +++ b/src/components/pages/master-data/kandang/KandangsTable.tsx @@ -14,10 +14,11 @@ 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 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'; @@ -258,27 +259,42 @@ const KandangsTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(kandangs) && kandangs?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx b/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx new file mode 100644 index 00000000..65a759ac --- /dev/null +++ b/src/components/pages/master-data/kandang/skeleton/KandangTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Kandang } from '@/types/api/master-data/kandang'; +import { ColumnDef } from '@tanstack/react-table'; + +const KandangTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no kandang data displayed. Enter kandang data to get started.', +}: { + columns: ColumnDef[]; + 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 bea0fcdd..0b619079 100644 --- a/src/components/pages/master-data/location/LocationsTable.tsx +++ b/src/components/pages/master-data/location/LocationsTable.tsx @@ -14,10 +14,10 @@ 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 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'; @@ -253,27 +253,42 @@ const LocationsTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(locations) && locations?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/location/skeleton/LocationTableSkeleton.tsx b/src/components/pages/master-data/location/skeleton/LocationTableSkeleton.tsx new file mode 100644 index 00000000..c7f1c868 --- /dev/null +++ b/src/components/pages/master-data/location/skeleton/LocationTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Location } from '@/types/api/master-data/location'; +import { ColumnDef } from '@tanstack/react-table'; + +const LocationTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no location data displayed. Enter location data to get started.', +}: { + columns: ColumnDef[]; + 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 6636a697..8f15e529 100644 --- a/src/components/pages/master-data/nonstock/NonstocksTable.tsx +++ b/src/components/pages/master-data/nonstock/NonstocksTable.tsx @@ -14,10 +14,10 @@ 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 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'; @@ -258,27 +258,42 @@ const NonstocksTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(nonstocks) && nonstocks?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
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 3a872b7f..161ebed4 100644 --- a/src/components/pages/master-data/product-category/ProductCategoryTable.tsx +++ b/src/components/pages/master-data/product-category/ProductCategoryTable.tsx @@ -20,10 +20,10 @@ 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 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'; @@ -282,38 +282,43 @@ const ProductCategoryTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(productCategories) && - productCategories?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/product-category/skeleton/ProductCategoryTableSkeleton.tsx b/src/components/pages/master-data/product-category/skeleton/ProductCategoryTableSkeleton.tsx new file mode 100644 index 00000000..7ea817ec --- /dev/null +++ b/src/components/pages/master-data/product-category/skeleton/ProductCategoryTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { ProductCategory } from '@/types/api/master-data/product-category'; +import { ColumnDef } from '@tanstack/react-table'; + +const ProductCategoryTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no product category data displayed. Enter product category data to get started.', +}: { + columns: ColumnDef[]; + 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 c4a5a87b..08b585f0 100644 --- a/src/components/pages/master-data/product/ProductTable.tsx +++ b/src/components/pages/master-data/product/ProductTable.tsx @@ -14,10 +14,11 @@ 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 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, formatCurrency } from '@/lib/helper'; +import { formatCurrency } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -303,27 +304,42 @@ const ProductsTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(products) && products?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
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 09e83fd1..0ff8b594 100644 --- a/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx +++ b/src/components/pages/master-data/production-standard/ProductionStandardTable.tsx @@ -13,10 +13,10 @@ 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 { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; const RowOptionsMenu = ({ @@ -201,25 +201,38 @@ const ProductionStandardTable = () => { {/* Table Section */}
- - data={ - isResponseSuccess(productionStandards) - ? productionStandards.data - : [] - } - columns={productionStandardColumns} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(productionStandards) && - productionStandards?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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 e5b225ec..2b6cb227 100644 --- a/src/components/pages/master-data/supplier/SupplierTable.tsx +++ b/src/components/pages/master-data/supplier/SupplierTable.tsx @@ -14,10 +14,10 @@ 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 SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton'; import { Supplier } from '@/types/api/master-data/supplier'; import { SupplierApi } from '@/services/api/master-data'; -import { cn } from '@/lib/helper'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { useTableFilter } from '@/services/hooks/useTableFilter'; @@ -273,27 +273,42 @@ const SuppliersTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(suppliers) && suppliers?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton.tsx b/src/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton.tsx new file mode 100644 index 00000000..a205d593 --- /dev/null +++ b/src/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Supplier } from '@/types/api/master-data/supplier'; +import { ColumnDef } from '@tanstack/react-table'; + +const SupplierTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no supplier data displayed. Enter supplier data to get started.', +}: { + columns: ColumnDef[]; + 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 064dc04e..aeaae276 100644 --- a/src/components/pages/master-data/uom/UomsTable.tsx +++ b/src/components/pages/master-data/uom/UomsTable.tsx @@ -14,10 +14,10 @@ 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 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'; @@ -241,24 +241,42 @@ const UomsTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': isResponseSuccess(uoms) && uoms?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/uom/skeleton/UomTableSkeleton.tsx b/src/components/pages/master-data/uom/skeleton/UomTableSkeleton.tsx new file mode 100644 index 00000000..05a42443 --- /dev/null +++ b/src/components/pages/master-data/uom/skeleton/UomTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Uom } from '@/types/api/master-data/uom'; +import { ColumnDef } from '@tanstack/react-table'; + +const UomTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no uom data displayed. Enter uom data to get started.', +}: { + columns: ColumnDef[]; + 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 17e1cc5e..c800e8eb 100644 --- a/src/components/pages/master-data/warehouse/WarehousesTable.tsx +++ b/src/components/pages/master-data/warehouse/WarehousesTable.tsx @@ -14,10 +14,10 @@ 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 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'; @@ -276,30 +276,43 @@ const WarehousesTable = () => { {/* Table Section */}
- - 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} - onPageSizeChange={setPageSize} - isLoading={isLoading} - sorting={sorting} - setSorting={setSorting} - className={{ - containerClassName: cn('p-3 mb-0', { - 'w-full': - isResponseSuccess(warehouses) && - warehouses?.data?.length === 0, - }), - headerColumnClassName: 'text-nowrap', - }} - /> + {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', + }} + /> + )}
diff --git a/src/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton.tsx b/src/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton.tsx new file mode 100644 index 00000000..5de5e66c --- /dev/null +++ b/src/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton.tsx @@ -0,0 +1,37 @@ +import DataStateSkeleton from '@/components/helper/skeleton/DataStateSkeleton'; +import Table from '@/components/Table'; +import { Warehouse } from '@/types/api/master-data/warehouse'; +import { ColumnDef } from '@tanstack/react-table'; + +const WarehouseTableSkeleton = ({ + columns, + icon, + title = 'No Data Available', + subtitle = 'There is no warehouse data displayed. Enter warehouse data to get started.', +}: { + columns: ColumnDef[]; + icon: React.ReactNode; + title?: string; + subtitle?: string; +}) => { + return ( +
+
+
+ +
+ + ); +}; + +export default WarehouseTableSkeleton; From 0e5d38f75c2e2b0777acca4c87577bf0d361b1d1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 13:57:56 +0700 Subject: [PATCH 28/30] refactor(FE): Update field name from 'isExpedition' to 'flags' in handler --- src/components/pages/master-data/nonstock/form/NonstockForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx index ccf0ecbe..3674ec09 100644 --- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx +++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx @@ -184,7 +184,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => { }; const expeditionChangeHandler = (e: React.ChangeEvent) => { - formik.setFieldValue('isExpedition', e.target.value === 'true'); + formik.setFieldValue('flags', e.target.value === 'true'); }; useEffect(() => { From eafcfd2f2851f16a61446974de9da3475d5f8fe4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 14:06:12 +0700 Subject: [PATCH 29/30] refactor(FE): Fix null check for product_flag_mapping in ProductForm --- src/components/pages/master-data/product/form/ProductForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index aa5217c5..11e4b11f 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -74,7 +74,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { ); const productFlagMapping: ProductFlagMapping | null = useMemo(() => { - if (constantsError || !constants) { + if (constantsError || !constants?.product_flag_mapping) { return PRODUCT_FLAG_MAPPING as unknown as ProductFlagMapping; } return constants.product_flag_mapping; From c6b906a28e5db17e7074b8af6ef887e48ec32094 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 2 Mar 2026 14:12:42 +0700 Subject: [PATCH 30/30] refactor(FE): Fix loading state for flag and sub-flag dropdowns --- src/components/pages/master-data/product/form/ProductForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pages/master-data/product/form/ProductForm.tsx b/src/components/pages/master-data/product/form/ProductForm.tsx index 11e4b11f..01fa192c 100644 --- a/src/components/pages/master-data/product/form/ProductForm.tsx +++ b/src/components/pages/master-data/product/form/ProductForm.tsx @@ -491,7 +491,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { formik.setFieldValue('sub_flags', []); }} options={productFlagMapping?.flags ?? []} - isLoading={isLoadingConstants} + isLoading={isLoadingConstants && !productFlagMapping} isError={formik.touched.flag && Boolean(formik.errors.flag)} errorMessage={formik.errors.flag as string} isDisabled={type === 'detail'} @@ -512,7 +512,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => { ); }} options={subFlagOptions} - isLoading={isLoadingConstants} + isLoading={isLoadingConstants && !productFlagMapping} isError={ formik.touched.sub_flags && Boolean(formik.errors.sub_flags) }