mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/global-search-store' into 'development'
[FEAT/FE] Add Global Search Store and Filter for Each Table Index (All Module) See merge request mbugroup/lti-web-client!340
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState, useMemo } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { useRouter } from 'next/navigation';
|
||||
@@ -91,6 +93,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ClosingsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
// ===== ROUTER =====
|
||||
const router = useRouter();
|
||||
|
||||
@@ -289,8 +294,17 @@ const ClosingsTable = () => {
|
||||
);
|
||||
}, [formik.values.project_status, projectStatusOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('closing-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
// ===== SEARCH CHANGE HANDLER =====
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
CellContext,
|
||||
@@ -151,6 +153,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ExpensesTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -507,7 +512,16 @@ const ExpensesTable = () => {
|
||||
setIsRejectLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('expense-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef, ColumnSort, SortingState } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
import Button from '@/components/Button';
|
||||
import Table from '@/components/Table';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatNumber, formatDate, formatCurrency } from '@/lib/helper';
|
||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { InventoryAdjustment } from '@/types/api/inventory/adjustment';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import InventoryAdjustmentTableSkeleton from '@/components/pages/inventory/adjustment/skeleton/InventoryAdjustmentTableSkeleton';
|
||||
import { TRANSACTION_SUBTYPE_OPTIONS } from '@/config/constant';
|
||||
|
||||
import {
|
||||
AdjustmentFilterSchema,
|
||||
AdjustmentFilterType,
|
||||
} from '@/components/pages/inventory/adjustment/filter/AdjustmentFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
|
||||
const InventoryAdjustmentTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -30,6 +56,9 @@ const InventoryAdjustmentTable = () => {
|
||||
productSort: '',
|
||||
warehouseSort: '',
|
||||
stockSort: '',
|
||||
productFilter: '',
|
||||
warehouseFilter: '',
|
||||
transactionTypeFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -38,9 +67,133 @@ const InventoryAdjustmentTable = () => {
|
||||
productSort: 'sort_product',
|
||||
warehouseSort: 'sort_warehouse',
|
||||
stockSort: 'sort_stock',
|
||||
productFilter: 'product_id',
|
||||
warehouseFilter: 'warehouse_id',
|
||||
transactionTypeFilter: 'transaction_type',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<AdjustmentFilterType>({
|
||||
initialValues: {
|
||||
product_id: null,
|
||||
warehouse_id: null,
|
||||
transaction_type: null,
|
||||
},
|
||||
validationSchema: AdjustmentFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('productFilter', values.product_id || '');
|
||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||
updateFilter('transactionTypeFilter', values.transaction_type || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('productFilter', '');
|
||||
updateFilter('warehouseFilter', '');
|
||||
updateFilter('transactionTypeFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== PRODUCT OPTIONS =====
|
||||
const {
|
||||
setInputValue: setProductInputValue,
|
||||
options: productOptions,
|
||||
isLoadingOptions: isLoadingProductOptions,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect<Product>(
|
||||
filterModal.open ? ProductApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== WAREHOUSE OPTIONS =====
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
loadMore: loadMoreWarehouses,
|
||||
} = useSelect<Warehouse>(
|
||||
filterModal.open ? WarehouseApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== TRANSACTION TYPE OPTIONS =====
|
||||
const transactionTypeOptions = useMemo(() => {
|
||||
return [
|
||||
{ value: 'increase', label: 'Increase' },
|
||||
{ value: 'decrease', label: 'Decrease' },
|
||||
];
|
||||
}, []);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterProductChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const product = val as OptionType | null;
|
||||
const productId = product?.value ? String(product.value) : null;
|
||||
formik.setFieldValue('product_id', productId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterWarehouseChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const warehouse = val as OptionType | null;
|
||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
||||
formik.setFieldValue('warehouse_id', warehouseId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterTransactionTypeChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const type = val as OptionType | null;
|
||||
const typeValue = type?.value ? String(type.value) : null;
|
||||
formik.setFieldValue('transaction_type', typeValue);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const productIdValue = useMemo(() => {
|
||||
if (!formik.values.product_id) return null;
|
||||
return (
|
||||
productOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.product_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.product_id, productOptions]);
|
||||
|
||||
const warehouseIdValue = useMemo(() => {
|
||||
if (!formik.values.warehouse_id) return null;
|
||||
return (
|
||||
warehouseOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
||||
|
||||
const transactionTypeValue = useMemo(() => {
|
||||
if (!formik.values.transaction_type) return null;
|
||||
return (
|
||||
transactionTypeOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.transaction_type
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.transaction_type, transactionTypeOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const { data: inventoryAdjustments, isLoading } = useSWR(
|
||||
`${InventoryAdjustmentApi.basePath}${getTableFilterQueryString()}`,
|
||||
InventoryAdjustmentApi.getAllFetcher
|
||||
@@ -48,6 +201,19 @@ const InventoryAdjustmentTable = () => {
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('inventory-adjustment-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
const inventoryAdjustmentsColumns: ColumnDef<InventoryAdjustment>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -182,76 +348,200 @@ const InventoryAdjustmentTable = () => {
|
||||
}, [sorting, updateSortingFilter]);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{/* Header Section */}
|
||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||
<RequirePermission permissions='lti.inventory.create'>
|
||||
<Button
|
||||
href='/inventory/adjustment/add'
|
||||
color='primary'
|
||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||
>
|
||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||
Add Adjustment
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
<div className='flex flex-col mb-4'>
|
||||
{isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
<>
|
||||
<div className='w-full'>
|
||||
{/* Header Section */}
|
||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||
{/* Action Buttons */}
|
||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||
<RequirePermission permissions='lti.inventory.create'>
|
||||
<Button
|
||||
href='/inventory/adjustment/add'
|
||||
color='primary'
|
||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||
>
|
||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||
Add Adjustment
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
) : !isResponseSuccess(inventoryAdjustments) ||
|
||||
inventoryAdjustments.data?.length === 0 ? (
|
||||
<div className='p-3'>
|
||||
<InventoryAdjustmentTableSkeleton
|
||||
columns={inventoryAdjustmentsColumns}
|
||||
icon={
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
<Icon
|
||||
icon='heroicons:document-text'
|
||||
className='text-white'
|
||||
icon='heroicons:magnifying-glass'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={[
|
||||
'page',
|
||||
'pageSize',
|
||||
'search',
|
||||
'productCategorySort',
|
||||
'productSort',
|
||||
'warehouseSort',
|
||||
'stockSort',
|
||||
]}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table<InventoryAdjustment>
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
<div className='flex flex-col mb-4'>
|
||||
{isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : !isResponseSuccess(inventoryAdjustments) ||
|
||||
inventoryAdjustments.data?.length === 0 ? (
|
||||
<div className='p-3'>
|
||||
<InventoryAdjustmentTableSkeleton
|
||||
columns={inventoryAdjustmentsColumns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:document-text'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table<InventoryAdjustment>
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
options={productOptions}
|
||||
value={productIdValue}
|
||||
onChange={handleFilterProductChange}
|
||||
onInputChange={setProductInputValue}
|
||||
isLoading={isLoadingProductOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreProducts}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Gudang'
|
||||
placeholder='Pilih Gudang'
|
||||
options={warehouseOptions}
|
||||
value={warehouseIdValue}
|
||||
onChange={handleFilterWarehouseChange}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
<SelectInputRadio
|
||||
label='Tipe Transaksi'
|
||||
placeholder='Pilih Tipe Transaksi'
|
||||
options={transactionTypeOptions}
|
||||
value={transactionTypeValue}
|
||||
onChange={handleFilterTransactionTypeChange}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const AdjustmentFilterSchema = object().shape({
|
||||
product_id: string().nullable(),
|
||||
warehouse_id: string().nullable(),
|
||||
transaction_type: string().nullable(),
|
||||
});
|
||||
|
||||
export type AdjustmentFilterType = {
|
||||
product_id: string | null;
|
||||
warehouse_id: string | null;
|
||||
transaction_type: string | null;
|
||||
};
|
||||
@@ -776,6 +776,16 @@ const InventoryAdjustmentForm = ({
|
||||
>
|
||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||
</section>
|
||||
{InventoryAdjustmentFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{InventoryAdjustmentFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex flex-col sm:flex-row sm:justify-end gap-2 mt-4'>
|
||||
{type !== 'detail' && (
|
||||
<>
|
||||
@@ -799,16 +809,6 @@ const InventoryAdjustmentForm = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{InventoryAdjustmentFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{InventoryAdjustmentFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
</>
|
||||
|
||||
@@ -1,22 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { SortingState, CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import Table from '@/components/Table';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { Movement } from '@/types/api/inventory/movement';
|
||||
import { MovementApi } from '@/services/api/inventory';
|
||||
import { WarehouseApi, ProductApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import Button from '@/components/Button';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
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';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import {
|
||||
MovementFilterSchema,
|
||||
MovementFilterType,
|
||||
} from '@/components/pages/inventory/movement/filter/MovementFilter';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -70,6 +90,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const MovementTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -79,13 +102,109 @@ const MovementTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
productFilter: '',
|
||||
warehouseFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
productFilter: 'product_id',
|
||||
warehouseFilter: 'warehouse_id',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<MovementFilterType>({
|
||||
initialValues: {
|
||||
product_id: null,
|
||||
warehouse_id: null,
|
||||
},
|
||||
validationSchema: MovementFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('productFilter', values.product_id || '');
|
||||
updateFilter('warehouseFilter', values.warehouse_id || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('productFilter', '');
|
||||
updateFilter('warehouseFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== PRODUCT OPTIONS =====
|
||||
const {
|
||||
setInputValue: setProductInputValue,
|
||||
options: productOptions,
|
||||
isLoadingOptions: isLoadingProductOptions,
|
||||
loadMore: loadMoreProducts,
|
||||
} = useSelect<Product>(
|
||||
filterModal.open ? ProductApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== WAREHOUSE OPTIONS =====
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
loadMore: loadMoreWarehouses,
|
||||
} = useSelect<Warehouse>(
|
||||
filterModal.open ? WarehouseApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterProductChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const product = val as OptionType | null;
|
||||
const productId = product?.value ? String(product.value) : null;
|
||||
formik.setFieldValue('product_id', productId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterWarehouseChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const warehouse = val as OptionType | null;
|
||||
const warehouseId = warehouse?.value ? String(warehouse.value) : null;
|
||||
formik.setFieldValue('warehouse_id', warehouseId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const productIdValue = useMemo(() => {
|
||||
if (!formik.values.product_id) return null;
|
||||
return (
|
||||
productOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.product_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.product_id, productOptions]);
|
||||
|
||||
const warehouseIdValue = useMemo(() => {
|
||||
if (!formik.values.warehouse_id) return null;
|
||||
return (
|
||||
warehouseOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.warehouse_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.warehouse_id, warehouseOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const { data: movements, isLoading } = useSWR(
|
||||
@@ -93,7 +212,16 @@ const MovementTable = () => {
|
||||
MovementApi.getAllFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('movement-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -160,85 +288,174 @@ const MovementTable = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{/* Header Section */}
|
||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||
{/* Action Buttons */}
|
||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||
<RequirePermission permissions='lti.inventory.transfer.create'>
|
||||
<Button
|
||||
href='/inventory/movement/add'
|
||||
color='primary'
|
||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||
>
|
||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||
Add Movement
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Movement'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
<Icon icon='heroicons:magnifying-glass' width={20} height={20} />
|
||||
}
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
<div className='flex flex-col mb-4'>
|
||||
{isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
<>
|
||||
<div className='w-full'>
|
||||
{/* Header Section */}
|
||||
<div className='w-full p-3 flex flex-row justify-between gap-3 flex-wrap border-b border-base-content/10'>
|
||||
{/* Action Buttons */}
|
||||
<div className='w-fit flex flex-row gap-3 flex-wrap'>
|
||||
<RequirePermission permissions='lti.inventory.transfer.create'>
|
||||
<Button
|
||||
href='/inventory/movement/add'
|
||||
color='primary'
|
||||
className='px-3 py-2.5 w-fit text-sm text-base-100 rounded-lg shadow-sm'
|
||||
>
|
||||
<Icon icon='heroicons:plus' width={20} height={20} />
|
||||
Add Movement
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
) : !isResponseSuccess(movements) || movements.data?.length === 0 ? (
|
||||
<div className='p-3'>
|
||||
<MovementTableSkeleton
|
||||
columns={movementColumns}
|
||||
icon={
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
<Icon
|
||||
icon='heroicons:document-text'
|
||||
className='text-white'
|
||||
icon='heroicons:magnifying-glass'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table<Movement>
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
<div className='flex flex-col mb-4'>
|
||||
{isLoading ? (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
) : !isResponseSuccess(movements) || movements.data?.length === 0 ? (
|
||||
<div className='p-3'>
|
||||
<MovementTableSkeleton
|
||||
columns={movementColumns}
|
||||
icon={
|
||||
<Icon
|
||||
icon='heroicons:document-text'
|
||||
className='text-white'
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Table<Movement>
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
options={productOptions}
|
||||
value={productIdValue}
|
||||
onChange={handleFilterProductChange}
|
||||
onInputChange={setProductInputValue}
|
||||
isLoading={isLoadingProductOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreProducts}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
<SelectInput
|
||||
label='Gudang'
|
||||
placeholder='Pilih Gudang'
|
||||
options={warehouseOptions}
|
||||
value={warehouseIdValue}
|
||||
onChange={handleFilterWarehouseChange}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreWarehouses}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const MovementFilterSchema = object().shape({
|
||||
product_id: string().nullable(),
|
||||
warehouse_id: string().nullable(),
|
||||
});
|
||||
|
||||
export type MovementFilterType = {
|
||||
product_id: string | null;
|
||||
warehouse_id: string | null;
|
||||
};
|
||||
@@ -8,10 +8,12 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
|
||||
import { InventoryProductApi } from '@/services/api/inventory';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
@@ -69,6 +71,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const InventoryProductTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -92,7 +97,16 @@ const InventoryProductTable = () => {
|
||||
InventoryProductApi.getAllFetcher
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('inventory-product-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -189,7 +203,7 @@ const InventoryProductTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Produk'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Area } from '@/types/api/master-data/area';
|
||||
import { AreaApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const AreasTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const AreasTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -132,7 +137,16 @@ const AreasTable = () => {
|
||||
const [selectedArea, setSelectedArea] = useState<Area | undefined>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('areas-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -219,7 +233,7 @@ const AreasTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Area'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Bank } from '@/types/api/master-data/bank';
|
||||
import { BankApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const BanksTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const BanksTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -132,7 +137,16 @@ const BanksTable = () => {
|
||||
const [selectedBank, setSelectedBank] = useState<Bank | undefined>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('banks-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -232,7 +246,7 @@ const BanksTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Bank'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Customer } from '@/types/api/master-data/customer';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const CustomersTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const CustomersTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -134,7 +139,16 @@ const CustomersTable = () => {
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('customers-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -239,7 +253,7 @@ const CustomersTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Customer'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Flock } from '@/types/api/master-data/flock';
|
||||
import { FlockApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const FlockTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const FlockTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -134,7 +139,16 @@ const FlockTable = () => {
|
||||
);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('flocks-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -227,7 +241,7 @@ const FlockTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Flock'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,26 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { 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 KandangTableSkeleton from '@/components/pages/master-data/kandang/skeleton/KandangTableSkeleton';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { KandangApi } from '@/services/api/master-data';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||
import { UserApi } from '@/services/api/user';
|
||||
import { User } from '@/types/api/api-general';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
KandangFilterSchema,
|
||||
KandangFilterType,
|
||||
} from '@/components/pages/master-data/kandang/filter/KandangFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -102,6 +122,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const KandangsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -111,13 +134,110 @@ const KandangsTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
locationFilter: '',
|
||||
picFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
locationFilter: 'location_id',
|
||||
picFilter: 'pic_id',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<KandangFilterType>({
|
||||
initialValues: {
|
||||
location_id: null,
|
||||
pic_id: null,
|
||||
},
|
||||
validationSchema: KandangFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('locationFilter', values.location_id || '');
|
||||
updateFilter('picFilter', values.pic_id || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('locationFilter', '');
|
||||
updateFilter('picFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== LOCATION OPTIONS =====
|
||||
const {
|
||||
setInputValue: setLocationInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(
|
||||
filterModal.open ? LocationApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== PIC OPTIONS =====
|
||||
const {
|
||||
setInputValue: setPicInputValue,
|
||||
options: picOptions,
|
||||
isLoadingOptions: isLoadingPicOptions,
|
||||
loadMore: loadMorePics,
|
||||
} = useSelect<User>(
|
||||
filterModal.open ? UserApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterLocationChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const location = val as OptionType | null;
|
||||
const locationId = location?.value ? String(location.value) : null;
|
||||
|
||||
formik.setFieldValue('location_id', locationId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterPicChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const pic = val as OptionType | null;
|
||||
const picId = pic?.value ? String(pic.value) : null;
|
||||
|
||||
formik.setFieldValue('pic_id', picId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const locationIdValue = useMemo(() => {
|
||||
if (!formik.values.location_id) return null;
|
||||
return (
|
||||
locationOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.location_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.location_id, locationOptions]);
|
||||
|
||||
const picIdValue = useMemo(() => {
|
||||
if (!formik.values.pic_id) return null;
|
||||
return (
|
||||
picOptions.find((opt) => String(opt.value) === formik.values.pic_id) ||
|
||||
null
|
||||
);
|
||||
}, [formik.values.pic_id, picOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -135,7 +255,16 @@ const KandangsTable = () => {
|
||||
);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('kandangs-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -233,11 +362,11 @@ const KandangsTable = () => {
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Kandang'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
@@ -254,6 +383,13 @@ const KandangsTable = () => {
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -312,6 +448,81 @@ const KandangsTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi'
|
||||
options={locationOptions}
|
||||
value={locationIdValue}
|
||||
onChange={handleFilterLocationChange}
|
||||
onInputChange={setLocationInputValue}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInputRadio
|
||||
label='PIC'
|
||||
placeholder='Pilih PIC'
|
||||
options={picOptions}
|
||||
value={picIdValue}
|
||||
onChange={handleFilterPicChange}
|
||||
onInputChange={setPicInputValue}
|
||||
isLoading={isLoadingPicOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMorePics}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const KandangFilterSchema = object().shape({
|
||||
location_id: string().nullable(),
|
||||
pic_id: string().nullable(),
|
||||
});
|
||||
|
||||
export type KandangFilterType = {
|
||||
location_id: string | null;
|
||||
pic_id: string | null;
|
||||
};
|
||||
@@ -1,25 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { 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 LocationTableSkeleton from '@/components/pages/master-data/location/skeleton/LocationTableSkeleton';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
import { LocationApi } from '@/services/api/master-data';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { LocationApi, AreaApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
LocationFilterSchema,
|
||||
LocationFilterType,
|
||||
} from '@/components/pages/master-data/location/filter/LocationFilter';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +118,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const LocationsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -110,14 +130,72 @@ const LocationsTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
areaFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
areaFilter: 'area_id',
|
||||
},
|
||||
});
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<LocationFilterType>({
|
||||
initialValues: {
|
||||
area_id: null,
|
||||
},
|
||||
validationSchema: LocationFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('areaFilter', values.area_id || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('areaFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== AREA OPTIONS =====
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(
|
||||
filterModal.open ? AreaApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterAreaChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const area = val as OptionType | null;
|
||||
const areaId = area?.value ? String(area.value) : null;
|
||||
|
||||
formik.setFieldValue('area_id', areaId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const areaIdValue = useMemo(() => {
|
||||
if (!formik.values.area_id) return null;
|
||||
return (
|
||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||
null
|
||||
);
|
||||
}, [formik.values.area_id, areaOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
const {
|
||||
data: locations,
|
||||
@@ -134,7 +212,18 @@ const LocationsTable = () => {
|
||||
>(undefined);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('locations-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -227,11 +316,11 @@ const LocationsTable = () => {
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Location'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
@@ -248,6 +337,13 @@ const LocationsTable = () => {
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -306,6 +402,68 @@ const LocationsTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
value={areaIdValue}
|
||||
onChange={handleFilterAreaChange}
|
||||
onInputChange={setAreaInputValue}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const LocationFilterSchema = object().shape({
|
||||
area_id: string().nullable(),
|
||||
});
|
||||
|
||||
export type LocationFilterType = {
|
||||
area_id: string | null;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||
import { NonstockApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const NonstocksTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const NonstocksTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -117,6 +122,14 @@ const NonstocksTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('nonstocks-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -135,6 +148,7 @@ const NonstocksTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -236,7 +250,7 @@ const NonstocksTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Nonstock'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -108,8 +109,8 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProductCategoryTable = () => {
|
||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
||||
const previousPathRef = useRef<string | null>(null);
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
@@ -119,7 +120,7 @@ const ProductCategoryTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: searchValue,
|
||||
search: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -127,6 +128,10 @@ const ProductCategoryTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -216,26 +221,8 @@ const ProductCategoryTable = () => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Store current path on mount
|
||||
previousPathRef.current = window.location.pathname;
|
||||
|
||||
return () => {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// if both paths are within /master-data/product-category module
|
||||
const isCurrentPathProductCategory = currentPath.includes(
|
||||
'/master-data/product-category'
|
||||
);
|
||||
const isPreviousPathProductCategory = previousPathRef.current?.includes(
|
||||
'/master-data/product-category'
|
||||
);
|
||||
|
||||
// reset if we outside product category module entirely
|
||||
if (isPreviousPathProductCategory && !isCurrentPathProductCategory) {
|
||||
resetSearchValue();
|
||||
}
|
||||
};
|
||||
}, [resetSearchValue]);
|
||||
setTableState('product-category-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -260,7 +247,7 @@ const ProductCategoryTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Product Category'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,26 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { 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 ProductTableSkeleton from '@/components/pages/master-data/product/skeleton/ProductTableSkeleton';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { ProductApi } from '@/services/api/master-data';
|
||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||
import { ProductApi, ProductCategoryApi } from '@/services/api/master-data';
|
||||
import { formatCurrency } from '@/lib/helper';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
ProductFilterSchema,
|
||||
ProductFilterType,
|
||||
} from '@/components/pages/master-data/product/filter/ProductFilter';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -102,6 +119,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProductsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -111,13 +131,78 @@ const ProductsTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
productCategoryFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
productCategoryFilter: 'product_category_id',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<ProductFilterType>({
|
||||
initialValues: {
|
||||
product_category_id: null,
|
||||
},
|
||||
validationSchema: ProductFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('productCategoryFilter', values.product_category_id || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('productCategoryFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== PRODUCT CATEGORY OPTIONS =====
|
||||
const {
|
||||
setInputValue: setProductCategoryInputValue,
|
||||
options: productCategoryOptions,
|
||||
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||
loadMore: loadMoreProductCategories,
|
||||
} = useSelect<ProductCategory>(
|
||||
filterModal.open ? ProductCategoryApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterProductCategoryChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const category = val as OptionType | null;
|
||||
const categoryId = category?.value ? String(category.value) : null;
|
||||
|
||||
formik.setFieldValue('product_category_id', categoryId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const productCategoryIdValue = useMemo(() => {
|
||||
if (!formik.values.product_category_id) return null;
|
||||
return (
|
||||
productCategoryOptions.find(
|
||||
(opt) => String(opt.value) === formik.values.product_category_id
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.product_category_id, productCategoryOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -135,7 +220,12 @@ const ProductsTable = () => {
|
||||
);
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('product-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -278,11 +368,11 @@ const ProductsTable = () => {
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Produk'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
@@ -299,6 +389,13 @@ const ProductsTable = () => {
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -357,6 +454,68 @@ const ProductsTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Kategori Produk'
|
||||
placeholder='Pilih Kategori Produk'
|
||||
options={productCategoryOptions}
|
||||
value={productCategoryIdValue}
|
||||
onChange={handleFilterProductCategoryChange}
|
||||
onInputChange={setProductCategoryInputValue}
|
||||
isLoading={isLoadingProductCategoryOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreProductCategories}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const ProductFilterSchema = object().shape({
|
||||
product_category_id: string().nullable(),
|
||||
});
|
||||
|
||||
export type ProductFilterType = {
|
||||
product_category_id: string | null;
|
||||
};
|
||||
@@ -1,23 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import PopoverButton from '@/components/popover/PopoverButton';
|
||||
import PopoverContent from '@/components/popover/PopoverContent';
|
||||
import ProductionStandardTableSkeleton from '@/components/pages/master-data/production-standard/skeleton/ProductionStandardTableSkeleton';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { ProductionStandard } from '@/types/api/master-data/production-standard';
|
||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
ProductionStandardFilterSchema,
|
||||
ProductionStandardFilterType,
|
||||
} from '@/components/pages/master-data/production-standard/filter/ProductionStandardFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -99,6 +110,85 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProductionStandardTable = () => {
|
||||
const { setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
projectCategoryFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
projectCategoryFilter: 'project_category',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<ProductionStandardFilterType>({
|
||||
initialValues: {
|
||||
project_category: null,
|
||||
},
|
||||
validationSchema: ProductionStandardFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('projectCategoryFilter', values.project_category || '');
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('projectCategoryFilter', '');
|
||||
},
|
||||
});
|
||||
|
||||
// ===== PROJECT CATEGORY OPTIONS (GROWING or LAYING) =====
|
||||
const projectCategoryOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'GROWING', label: 'Growing' },
|
||||
{ value: 'LAYING', label: 'Laying' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterProjectCategoryChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType | null;
|
||||
const category = option?.value ? String(option.value) : null;
|
||||
|
||||
formik.setFieldValue('project_category', category);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const projectCategoryValue = useMemo(() => {
|
||||
if (!formik.values.project_category) return null;
|
||||
return (
|
||||
projectCategoryOptions.find(
|
||||
(opt) => opt.value === formik.values.project_category
|
||||
) || null
|
||||
);
|
||||
}, [formik.values.project_category, projectCategoryOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('production-standard-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -106,7 +196,7 @@ const ProductionStandardTable = () => {
|
||||
isLoading,
|
||||
mutate: refreshProductionStandards,
|
||||
} = useSWR(
|
||||
`${ProductionStandardApi.basePath}`,
|
||||
`${ProductionStandardApi.basePath}${getTableFilterQueryString()}`,
|
||||
ProductionStandardApi.getAllFetcher
|
||||
);
|
||||
|
||||
@@ -140,7 +230,10 @@ const ProductionStandardTable = () => {
|
||||
() => [
|
||||
{
|
||||
header: 'No',
|
||||
cell: (props) => props.row.index + 1,
|
||||
cell: (props) =>
|
||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||
props.row.index +
|
||||
1,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
@@ -176,7 +269,7 @@ const ProductionStandardTable = () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[deleteModal]
|
||||
[tableFilterState.pageSize, tableFilterState.page, deleteModal]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -197,6 +290,16 @@ const ProductionStandardTable = () => {
|
||||
</Button>
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table Section */}
|
||||
@@ -224,6 +327,11 @@ const ProductionStandardTable = () => {
|
||||
<Table<ProductionStandard>
|
||||
data={productionStandards.data}
|
||||
columns={productionStandardColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
page={productionStandards?.meta?.page ?? 0}
|
||||
totalItems={productionStandards?.meta?.total_results ?? 0}
|
||||
onPageChange={setPage}
|
||||
onPageSizeChange={setPageSize}
|
||||
isLoading={false}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
@@ -250,6 +358,65 @@ const ProductionStandardTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInputRadio
|
||||
label='Kategori'
|
||||
placeholder='Pilih Kategori'
|
||||
options={projectCategoryOptions}
|
||||
value={projectCategoryValue}
|
||||
onChange={handleFilterProjectCategoryChange}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { string, object } from 'yup';
|
||||
|
||||
export const ProductionStandardFilterSchema = object().shape({
|
||||
project_category: string().nullable(),
|
||||
});
|
||||
|
||||
export type ProductionStandardFilterType = {
|
||||
project_category: string | null;
|
||||
};
|
||||
@@ -1,25 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { 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 SupplierTableSkeleton from '@/components/pages/master-data/supplier/skeleton/SupplierTableSkeleton';
|
||||
import SelectInput from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
SupplierFilterSchema,
|
||||
SupplierFilterType,
|
||||
} from '@/components/pages/master-data/supplier/filter/SupplierFilter';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +118,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const SuppliersTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -110,13 +130,126 @@ const SuppliersTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
categoryFilter: '',
|
||||
flagFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
categoryFilter: 'category_id',
|
||||
flagFilter: 'flag',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<SupplierFilterType>({
|
||||
initialValues: {
|
||||
category_id: null,
|
||||
flag: false,
|
||||
},
|
||||
validationSchema: SupplierFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('categoryFilter', values.category_id || '');
|
||||
updateFilter(
|
||||
'flagFilter',
|
||||
values.flag === true ? 'EKSPEDISI' : values.flag === false ? '' : ''
|
||||
);
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('categoryFilter', '');
|
||||
updateFilter('flagFilter', '');
|
||||
formik.setFieldValue('flag', false);
|
||||
},
|
||||
});
|
||||
|
||||
// ===== CATEGORY OPTIONS (SAPRONAK or BOP) =====
|
||||
const categoryOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'SAPRONAK', label: 'SAPRONAK' },
|
||||
{ value: 'BOP', label: 'BOP' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FLAG OPTIONS (EKSPEDISI) =====
|
||||
const flagOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'true', label: 'Ya' },
|
||||
{ value: 'false', label: 'Tidak' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterCategoryChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType | null;
|
||||
const categoryId = option?.value ? String(option.value) : null;
|
||||
|
||||
formik.setFieldValue('category_id', categoryId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterFlagChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType | null;
|
||||
const boolValue =
|
||||
option?.value === 'true'
|
||||
? true
|
||||
: option?.value === 'false'
|
||||
? false
|
||||
: null;
|
||||
|
||||
formik.setFieldValue('flag', boolValue);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const categoryIdValue = useMemo(() => {
|
||||
if (!formik.values.category_id) return null;
|
||||
return (
|
||||
categoryOptions.find((opt) => opt.value === formik.values.category_id) ||
|
||||
null
|
||||
);
|
||||
}, [formik.values.category_id, categoryOptions]);
|
||||
|
||||
const flagValue = useMemo(() => {
|
||||
if (formik.values.flag === null) return null;
|
||||
return (
|
||||
flagOptions.find((opt) => opt.value === String(formik.values.flag)) ||
|
||||
flagOptions[1]
|
||||
);
|
||||
}, [formik.values.flag, flagOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (filterModal.open) {
|
||||
const flagBoolValue =
|
||||
tableFilterState.flagFilter === 'EKSPEDISI' ? true : false;
|
||||
formik.setFieldValue('flag', flagBoolValue);
|
||||
}
|
||||
}, [filterModal.open, tableFilterState.flagFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('suppliers-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -135,6 +268,7 @@ const SuppliersTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -247,11 +381,11 @@ const SuppliersTable = () => {
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Supplier'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
@@ -268,6 +402,13 @@ const SuppliersTable = () => {
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -326,6 +467,74 @@ const SuppliersTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInputRadio
|
||||
label='Kategori'
|
||||
placeholder='Pilih Kategori'
|
||||
options={categoryOptions}
|
||||
value={categoryIdValue}
|
||||
onChange={handleFilterCategoryChange}
|
||||
isClearable
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInputRadio
|
||||
label='Flag Ekspedisi'
|
||||
placeholder='Pilih'
|
||||
options={flagOptions}
|
||||
value={flagValue}
|
||||
onChange={handleFilterFlagChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { string, boolean, object } from 'yup';
|
||||
|
||||
export const SupplierFilterSchema = object().shape({
|
||||
category_id: string().nullable(),
|
||||
flag: boolean().nullable(),
|
||||
});
|
||||
|
||||
export type SupplierFilterType = {
|
||||
category_id: string | null;
|
||||
flag: boolean | null;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -20,6 +20,8 @@ import { Uom } from '@/types/api/master-data/uom';
|
||||
import { UomApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +103,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const UomsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -109,7 +114,7 @@ const UomsTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
search: searchValue,
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
@@ -117,6 +122,14 @@ const UomsTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('uoms-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -133,6 +146,7 @@ const UomsTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -219,7 +233,7 @@ const UomsTable = () => {
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari UOM'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
|
||||
@@ -1,25 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import Modal, { 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 WarehouseTableSkeleton from '@/components/pages/master-data/warehouse/skeleton/WarehouseTableSkeleton';
|
||||
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { WarehouseApi } from '@/services/api/master-data';
|
||||
import { WarehouseApi, AreaApi } from '@/services/api/master-data';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import {
|
||||
WarehouseFilterSchema,
|
||||
WarehouseFilterType,
|
||||
} from '@/components/pages/master-data/warehouse/filter/WarehouseFilter';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import SelectInput, { useSelect } from '@/components/input/SelectInput';
|
||||
import SelectInputRadio from '@/components/input/SelectInputRadio';
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
popoverPosition = 'bottom',
|
||||
@@ -101,6 +120,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const WarehousesTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -110,13 +132,131 @@ const WarehousesTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
areaFilter: '',
|
||||
activeProjectFlockFilter: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
areaFilter: 'area_id',
|
||||
activeProjectFlockFilter: 'active_project_flock',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
// ===== FORMIK SETUP =====
|
||||
const formik = useFormik<WarehouseFilterType>({
|
||||
initialValues: {
|
||||
area_id: null,
|
||||
active_project_flock: false,
|
||||
},
|
||||
validationSchema: WarehouseFilterSchema,
|
||||
onSubmit: (values, { setSubmitting }) => {
|
||||
updateFilter('areaFilter', values.area_id || '');
|
||||
updateFilter(
|
||||
'activeProjectFlockFilter',
|
||||
values.active_project_flock === true ? 'true' : ''
|
||||
);
|
||||
filterModal.closeModal();
|
||||
setSubmitting(false);
|
||||
},
|
||||
onReset: () => {
|
||||
updateFilter('areaFilter', '');
|
||||
updateFilter('activeProjectFlockFilter', '');
|
||||
formik.setFieldValue('active_project_flock', false);
|
||||
},
|
||||
});
|
||||
|
||||
// ===== AREA OPTIONS =====
|
||||
const {
|
||||
setInputValue: setAreaInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(
|
||||
filterModal.open ? AreaApi.basePath : null,
|
||||
'id',
|
||||
'name',
|
||||
'search'
|
||||
);
|
||||
|
||||
// ===== ACTIVE PROJECT FLOCK OPTIONS =====
|
||||
const activeProjectFlockOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'true', label: 'Kandang Aktif' },
|
||||
{ value: 'false', label: 'Semua Kandang' },
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== FILTER HANDLERS =====
|
||||
const handleFilterAreaChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const area = val as OptionType | null;
|
||||
const areaId = area?.value ? String(area.value) : null;
|
||||
|
||||
formik.setFieldValue('area_id', areaId);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleFilterActiveProjectFlockChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const option = val as OptionType | null;
|
||||
const boolValue =
|
||||
option?.value === 'true'
|
||||
? true
|
||||
: option?.value === 'false'
|
||||
? false
|
||||
: null;
|
||||
|
||||
formik.setFieldValue('active_project_flock', boolValue);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
// ===== FILTER HELPERS =====
|
||||
const areaIdValue = useMemo(() => {
|
||||
if (!formik.values.area_id) return null;
|
||||
return (
|
||||
areaOptions.find((opt) => String(opt.value) === formik.values.area_id) ||
|
||||
null
|
||||
);
|
||||
}, [formik.values.area_id, areaOptions]);
|
||||
|
||||
const activeProjectFlockValue = useMemo(() => {
|
||||
if (formik.values.active_project_flock === null) return null;
|
||||
return (
|
||||
activeProjectFlockOptions.find(
|
||||
(opt) => opt.value === String(formik.values.active_project_flock)
|
||||
) || activeProjectFlockOptions[1]
|
||||
);
|
||||
}, [formik.values.active_project_flock, activeProjectFlockOptions]);
|
||||
|
||||
// ===== HANDLE FILTER MODAL OPEN =====
|
||||
const handleFilterModalOpen = () => {
|
||||
filterModal.openModal();
|
||||
formik.validateForm();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (filterModal.open) {
|
||||
const activeProjectFlockValue =
|
||||
tableFilterState.activeProjectFlockFilter === 'true' ? true : false; // Default ke false (Semua Kandang)
|
||||
formik.setFieldValue('active_project_flock', activeProjectFlockValue);
|
||||
}
|
||||
}, [filterModal.open]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('warehouses-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
@@ -135,6 +275,7 @@ const WarehousesTable = () => {
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
@@ -250,11 +391,11 @@ const WarehousesTable = () => {
|
||||
</RequirePermission>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{/* Search and Filter */}
|
||||
<div className='flex flex-1 flex-row justify-start sm:justify-end items-center gap-3 flex-wrap'>
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Warehouse'
|
||||
placeholder='Search'
|
||||
value={tableFilterState.search ?? ''}
|
||||
onChange={searchChangeHandler}
|
||||
startAdornment={
|
||||
@@ -271,6 +412,13 @@ const WarehousesTable = () => {
|
||||
'placeholder:font-semibold placeholder:text-base-content/50',
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonFilter
|
||||
values={tableFilterState}
|
||||
excludeFields={['page', 'pageSize', 'search']}
|
||||
onClick={handleFilterModalOpen}
|
||||
className='px-3 py-2.5'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -330,6 +478,77 @@ const WarehousesTable = () => {
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Filter Modal */}
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-[0.875rem] xl:max-w-4/12 max-w-sm',
|
||||
}}
|
||||
>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 border-b border-base-content/10 p-4'>
|
||||
<div className='flex items-center gap-2 text-primary'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-medium text-sm'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={filterModal.closeModal}
|
||||
className='text-base-content/50 hover:text-base-content transition-colors cursor-pointer'
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<form onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
|
||||
<div className='p-4 flex flex-col gap-1.5'>
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area'
|
||||
options={areaOptions}
|
||||
value={areaIdValue}
|
||||
onChange={handleFilterAreaChange}
|
||||
onInputChange={setAreaInputValue}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
|
||||
<SelectInputRadio
|
||||
label='Filter Berdasarkan'
|
||||
placeholder='Pilih'
|
||||
options={activeProjectFlockOptions}
|
||||
value={activeProjectFlockValue}
|
||||
onChange={handleFilterActiveProjectFlockChange}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className='flex justify-between items-center gap-4 p-4 border-t border-base-content/10 bg-gray-50'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='soft'
|
||||
className='rounded-lg text-base-content/65 bg-transparent border-none hover:bg-base-content/10 hover:text-base-content/65 transition-colors px-3 py-2'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
filterModal.closeModal();
|
||||
}}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
className='min-w-40 text-sm rounded-lg py-3 text-white font-semibold'
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Apply Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { string, boolean, object } from 'yup';
|
||||
|
||||
export const WarehouseFilterSchema = object().shape({
|
||||
area_id: string().nullable(),
|
||||
active_project_flock: boolean().nullable(),
|
||||
});
|
||||
|
||||
export type WarehouseFilterType = {
|
||||
area_id: string | null;
|
||||
active_project_flock: boolean | null;
|
||||
};
|
||||
@@ -21,8 +21,9 @@ import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import toast from 'react-hot-toast';
|
||||
import useSWR from 'swr';
|
||||
import { useFormik } from 'formik';
|
||||
@@ -148,6 +149,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const isSuccess = useProjectFlockStore((s) => s.isSuccess);
|
||||
const setIsSuccess = useProjectFlockStore((s) => s.setIsSuccess);
|
||||
const createdProjectFlock = useProjectFlockStore(
|
||||
@@ -416,7 +420,16 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
||||
setIsDeleteLoading(false);
|
||||
setRowSelection({});
|
||||
};
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('project-flock-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
const confirmApprovalHandler = async (
|
||||
|
||||
@@ -44,6 +44,7 @@ import Badge from '@/components/Badge';
|
||||
import StatusBadge from '@/components/helper/StatusBadge';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Color } from '@/types/theme';
|
||||
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||
|
||||
@@ -203,8 +204,8 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const RecordingTable = () => {
|
||||
const { searchValue, setSearchValue, resetSearchValue } = useUiStore();
|
||||
const previousPathRef = useRef<string | null>(null);
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
@@ -214,7 +215,7 @@ const RecordingTable = () => {
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: searchValue,
|
||||
search: '',
|
||||
areaFilter: '',
|
||||
locationFilter: '',
|
||||
kandangFilter: '',
|
||||
@@ -229,6 +230,10 @@ const RecordingTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
// ===== FILTER MODAL STATE =====
|
||||
const filterModal = useModal();
|
||||
|
||||
@@ -526,23 +531,8 @@ const RecordingTable = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
previousPathRef.current = window.location.pathname;
|
||||
|
||||
return () => {
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
const isCurrentPathRecording = currentPath.includes(
|
||||
'/production/recording'
|
||||
);
|
||||
const isPreviousPathRecording = previousPathRef.current?.includes(
|
||||
'/production/recording'
|
||||
);
|
||||
|
||||
if (isPreviousPathRecording && !isCurrentPathRecording) {
|
||||
resetSearchValue();
|
||||
}
|
||||
};
|
||||
}, [resetSearchValue]);
|
||||
setTableState('recording-table', pathname);
|
||||
}, [pathname]);
|
||||
|
||||
const searchChangeHandler = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
CellContext,
|
||||
@@ -121,6 +123,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const TransferToLayingsTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
@@ -413,7 +418,16 @@ const TransferToLayingsTable = () => {
|
||||
setIsRejectLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('transfer-to-laying-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
@@ -184,6 +185,8 @@ const UniformityChartWrapper = ({
|
||||
const UniformityTable = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const isSuccess = useUniformityStore((s) => s.isSuccess);
|
||||
const setIsSuccess = useUniformityStore((s) => s.setIsSuccess);
|
||||
const createdUniformity = useUniformityStore((s) => s.createdUniformity);
|
||||
@@ -218,6 +221,14 @@ const UniformityTable = () => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('uniformity-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||
const [selectedUniformity, setSelectedUniformity] = useState<
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useUiStore } from '@/stores/ui/ui.store';
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
@@ -139,6 +147,9 @@ const RowOptionsMenu = ({
|
||||
};
|
||||
|
||||
const PurchaseTable = () => {
|
||||
const { searchValue, setSearchValue, setTableState } = useUiStore();
|
||||
const pathname = usePathname();
|
||||
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
||||
@@ -385,11 +396,20 @@ const PurchaseTable = () => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
||||
|
||||
useEffect(() => {
|
||||
updateFilter('search', searchValue);
|
||||
}, [searchValue, updateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableState('purchase-table', pathname);
|
||||
}, [pathname, setTableState]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
setSearchValue(e.target.value);
|
||||
updateFilter('search', e.target.value);
|
||||
},
|
||||
[updateFilter]
|
||||
[updateFilter, setSearchValue]
|
||||
);
|
||||
|
||||
const pageSizeChangeHandler = useCallback(
|
||||
|
||||
@@ -175,26 +175,54 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
validateOnBlur: true,
|
||||
enableReinitialize: false,
|
||||
onSubmit: async (values) => {
|
||||
type ItemPayload = {
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
received_qty: number;
|
||||
vehicle_number?: string;
|
||||
expedition_vendor_id?: number;
|
||||
transport_per_item?: number;
|
||||
};
|
||||
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
notes: values.notes || '',
|
||||
items:
|
||||
values.items?.map((formItem) => {
|
||||
return {
|
||||
const item: ItemPayload = {
|
||||
purchase_item_id: formItem.purchase_item_id || 0,
|
||||
received_date: formItem.received_date || '',
|
||||
travel_number: formItem.travel_number || '',
|
||||
vehicle_number: formItem.vehicle_number || null,
|
||||
expedition_vendor_id: formItem.expedition_vendor_id || null,
|
||||
received_qty:
|
||||
typeof formItem.received_qty === 'string'
|
||||
? parseFloat(formItem.received_qty) || 0
|
||||
: formItem.received_qty || 0,
|
||||
transport_per_item:
|
||||
typeof formItem.transport_per_item === 'string'
|
||||
? parseFloat(formItem.transport_per_item) || 0
|
||||
: formItem.transport_per_item || null,
|
||||
};
|
||||
|
||||
if (
|
||||
formItem.vehicle_number &&
|
||||
formItem.vehicle_number.trim() !== ''
|
||||
) {
|
||||
item.vehicle_number = formItem.vehicle_number;
|
||||
}
|
||||
|
||||
if (
|
||||
formItem.expedition_vendor_id &&
|
||||
formItem.expedition_vendor_id !== 0
|
||||
) {
|
||||
item.expedition_vendor_id = formItem.expedition_vendor_id;
|
||||
}
|
||||
|
||||
const transportValue =
|
||||
typeof formItem.transport_per_item === 'string'
|
||||
? parseFloat(formItem.transport_per_item) || 0
|
||||
: formItem.transport_per_item || 0;
|
||||
if (transportValue > 0) {
|
||||
item.transport_per_item = transportValue;
|
||||
}
|
||||
|
||||
return item;
|
||||
}) || [],
|
||||
travel_documents:
|
||||
values.travel_documents
|
||||
@@ -258,7 +286,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
if (purchaseItems.length > 0 && initialValues?.items) {
|
||||
const updatedItems = initialValues.items.map((item) => {
|
||||
const expeditionVendorId =
|
||||
item.expedition_vendor_id || item.expedition_vendor?.id || 0;
|
||||
item.expedition_vendor_id || item.expedition_vendor?.id || null;
|
||||
|
||||
return {
|
||||
purchase_item: null,
|
||||
@@ -327,10 +355,14 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
formik.setFieldTouched(`items.${idx}.expedition_vendor`, true);
|
||||
formik.setFieldValue(`items.${idx}.expedition_vendor`, expeditionVendor);
|
||||
formik.setFieldTouched(`items.${idx}.expedition_vendor_id`, true);
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.expedition_vendor_id`,
|
||||
expeditionVendor?.value || 0
|
||||
);
|
||||
if (expeditionVendor?.value) {
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.expedition_vendor_id`,
|
||||
expeditionVendor.value
|
||||
);
|
||||
} else {
|
||||
formik.setFieldValue(`items.${idx}.expedition_vendor_id`, null);
|
||||
}
|
||||
};
|
||||
|
||||
// ===== PURCHASE ITEM OPERATIONS =====
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import { StateCreator } from 'zustand';
|
||||
import { TableUISlice } from '@/types/stores';
|
||||
|
||||
export interface TableState {
|
||||
key: string;
|
||||
path: string;
|
||||
searchValue: string;
|
||||
}
|
||||
|
||||
export interface TableUISlice {
|
||||
searchValue: string;
|
||||
setSearchValue: (value: string) => void;
|
||||
resetSearchValue: () => void;
|
||||
function getPathGroup(path: string): string {
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const subPaths = ['add', 'detail', 'edit'];
|
||||
const lastSegment = segments[segments.length - 1];
|
||||
|
||||
// If last segment is a sub-path or numeric ID, remove it
|
||||
if (
|
||||
lastSegment &&
|
||||
(subPaths.includes(lastSegment) || /^\d+$/.test(lastSegment))
|
||||
) {
|
||||
segments.pop();
|
||||
}
|
||||
|
||||
return segments.length > 0 ? '/' + segments.join('/') : path;
|
||||
}
|
||||
|
||||
function isSamePathGroup(
|
||||
currentPath: string,
|
||||
previousPath: string | null
|
||||
): boolean {
|
||||
if (!previousPath) return false;
|
||||
return getPathGroup(currentPath) === getPathGroup(previousPath);
|
||||
}
|
||||
|
||||
export const createTableUISlice: StateCreator<
|
||||
@@ -17,7 +38,10 @@ export const createTableUISlice: StateCreator<
|
||||
TableUISlice
|
||||
> = (set) => ({
|
||||
// Initial state
|
||||
key: '',
|
||||
path: '',
|
||||
searchValue: '',
|
||||
previousPath: null,
|
||||
|
||||
// Actions
|
||||
setSearchValue: (value) => set({ searchValue: value }),
|
||||
@@ -25,4 +49,27 @@ export const createTableUISlice: StateCreator<
|
||||
resetSearchValue: () => {
|
||||
return set({ searchValue: '' });
|
||||
},
|
||||
|
||||
setTableState: (key, path, searchValue = '') => {
|
||||
set((state) => {
|
||||
const isSameGroup = isSamePathGroup(path, state.path);
|
||||
|
||||
return {
|
||||
key,
|
||||
path,
|
||||
previousPath: state.path,
|
||||
// Reset search if path group changed, otherwise keep existing search
|
||||
searchValue: isSameGroup ? state.searchValue : searchValue,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
resetTableState: () => {
|
||||
set({
|
||||
key: '',
|
||||
path: '',
|
||||
searchValue: '',
|
||||
previousPath: null,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,9 +19,12 @@ export const useUiStore = create<UIStore>()(
|
||||
...createNavbarActionsSlice(...args),
|
||||
}),
|
||||
{
|
||||
name: 'ui-cache',
|
||||
name: 'search-store',
|
||||
partialize: (state) => ({
|
||||
key: state.key,
|
||||
path: state.path,
|
||||
searchValue: state.searchValue,
|
||||
previousPath: state.previousPath,
|
||||
}),
|
||||
}
|
||||
),
|
||||
|
||||
Vendored
+8
@@ -28,9 +28,17 @@ type DrawerUISlice = {
|
||||
};
|
||||
|
||||
type TableUISlice = {
|
||||
// State
|
||||
key: string;
|
||||
path: string;
|
||||
searchValue: string;
|
||||
previousPath: string | null;
|
||||
|
||||
// Actions
|
||||
setSearchValue: (value: string) => void;
|
||||
resetSearchValue: () => void;
|
||||
setTableState: (key: string, path: string, searchValue?: string) => void;
|
||||
resetTableState: () => void;
|
||||
};
|
||||
|
||||
// Navbar Actions Slice
|
||||
|
||||
Reference in New Issue
Block a user