mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'fix/product-stock-optimization' into 'development'
[FIX/FE] Product Stock Optimization See merge request mbugroup/lti-web-client!472
This commit is contained in:
@@ -1,9 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import { FormHeader } from '@/components/helper/form/FormHeader';
|
import { FormHeader } from '@/components/helper/form/FormHeader';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import { useModal } from '@/components/Modal';
|
||||||
|
import StockLogFilterModal from '@/components/pages/inventory/product/detail/StockLogFilterModal';
|
||||||
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
import StockLogTable from '@/components/pages/inventory/product/detail/StockLogTable';
|
||||||
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
import StockProductWarehouseTable from '@/components/pages/inventory/product/detail/StockProductWarehouseTable';
|
||||||
import { formatCurrency, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatNumber } from '@/lib/helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { InventoryProduct } from '@/types/api/inventory/product';
|
import { InventoryProduct } from '@/types/api/inventory/product';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
@@ -12,6 +19,35 @@ const InventoryProductDetail = ({
|
|||||||
}: {
|
}: {
|
||||||
inventoryProduct?: InventoryProduct;
|
inventoryProduct?: InventoryProduct;
|
||||||
}) => {
|
}) => {
|
||||||
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
const { state: filterState, updateFilter } = useTableFilter<{
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}>({
|
||||||
|
initial: {
|
||||||
|
warehouse_ids: [],
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
storeName: 'inventory-product-stock-log-filter',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredProductWarehouses = useMemo(() => {
|
||||||
|
const warehouses = inventoryProduct?.product_warehouses ?? [];
|
||||||
|
if (!filterState.warehouse_ids?.length) return warehouses;
|
||||||
|
const selectedIds = new Set(filterState.warehouse_ids.map((w) => w.value));
|
||||||
|
return warehouses.filter((pw) => selectedIds.has(pw.warehouse_id));
|
||||||
|
}, [inventoryProduct?.product_warehouses, filterState.warehouse_ids]);
|
||||||
|
|
||||||
|
const filterSubmitHandler = (values: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
}) => {
|
||||||
|
updateFilter('warehouse_ids', values.warehouse_ids, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterResetHandler = () => {
|
||||||
|
updateFilter('warehouse_ids', [], true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4 p-4'>
|
<div className='flex flex-col gap-4 p-4'>
|
||||||
<FormHeader
|
<FormHeader
|
||||||
@@ -104,13 +140,28 @@ const InventoryProductDetail = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
<RequirePermission permissions={'lti.inventory.stock_log.list'}>
|
||||||
{inventoryProduct?.product_warehouses?.map((productWarehouse) => (
|
<div className='flex justify-end'>
|
||||||
|
<ButtonFilter
|
||||||
|
values={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onClick={filterModal.openModal}
|
||||||
|
className='px-3 py-2.5'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{filteredProductWarehouses.map((productWarehouse) => (
|
||||||
<StockLogTable
|
<StockLogTable
|
||||||
key={productWarehouse.id}
|
key={productWarehouse.id}
|
||||||
productWarehouse={productWarehouse}
|
productWarehouse={productWarehouse}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
|
||||||
|
<StockLogFilterModal
|
||||||
|
ref={filterModal.ref}
|
||||||
|
productWarehouses={inventoryProduct?.product_warehouses ?? []}
|
||||||
|
initialValues={{ warehouse_ids: filterState.warehouse_ids }}
|
||||||
|
onSubmit={filterSubmitHandler}
|
||||||
|
onReset={filterResetHandler}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import Modal from '@/components/Modal';
|
||||||
|
import { ProductWarehouseStock } from '@/types/api/inventory/product';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import { RefObject, useCallback } from 'react';
|
||||||
|
|
||||||
|
interface StockLogFilterModalProps {
|
||||||
|
ref: RefObject<HTMLDialogElement | null>;
|
||||||
|
productWarehouses: ProductWarehouseStock[];
|
||||||
|
initialValues: {
|
||||||
|
warehouse_ids: OptionType<number>[];
|
||||||
|
};
|
||||||
|
onSubmit: (values: { warehouse_ids: OptionType<number>[] }) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StockLogFilterModal = ({
|
||||||
|
ref,
|
||||||
|
productWarehouses,
|
||||||
|
initialValues,
|
||||||
|
onSubmit,
|
||||||
|
onReset,
|
||||||
|
}: StockLogFilterModalProps) => {
|
||||||
|
const closeModalHandler = () => {
|
||||||
|
ref.current?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const warehouseOptions: OptionType<number>[] = productWarehouses.map(
|
||||||
|
(pw) => ({
|
||||||
|
label: pw.warehouse_name,
|
||||||
|
value: pw.warehouse_id,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
|
onSubmit: (values) => {
|
||||||
|
onSubmit(values);
|
||||||
|
closeModalHandler();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resetForm } = formik;
|
||||||
|
|
||||||
|
const formikResetHandler = useCallback(() => {
|
||||||
|
resetForm({ values: { warehouse_ids: [] } });
|
||||||
|
onReset();
|
||||||
|
closeModalHandler();
|
||||||
|
}, [resetForm, onReset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal ref={ref} className={{ modalBox: 'p-0 rounded-xl' }}>
|
||||||
|
<form
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formikResetHandler}
|
||||||
|
className='w-full flex flex-col'
|
||||||
|
>
|
||||||
|
<div className='p-4 flex items-center justify-between gap-2 border-b border-gray-300'>
|
||||||
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
|
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||||
|
<h3 className='text-sm font-medium'>Filter Stock Log</h3>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
onClick={closeModalHandler}
|
||||||
|
className='p-0 text-base-content/50 hover:text-base-content'
|
||||||
|
>
|
||||||
|
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex flex-col gap-1.5'>
|
||||||
|
<SelectInputCheckbox
|
||||||
|
label='Gudang'
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih gudang'
|
||||||
|
options={warehouseOptions}
|
||||||
|
value={formik.values.warehouse_ids}
|
||||||
|
onChange={(val) =>
|
||||||
|
formik.setFieldValue('warehouse_ids', val as OptionType<number>[])
|
||||||
|
}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='p-4 flex justify-between gap-4 border-t border-gray-300 bg-gray-100'>
|
||||||
|
<Button
|
||||||
|
type='reset'
|
||||||
|
variant='ghost'
|
||||||
|
color='none'
|
||||||
|
className='p-3 rounded-lg text-base-content/65'
|
||||||
|
>
|
||||||
|
Reset Filter
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type='submit'
|
||||||
|
className='p-3 rounded-lg w-fit sm:w-full max-w-40 text-base-100 text-sm'
|
||||||
|
>
|
||||||
|
Apply Filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StockLogFilterModal;
|
||||||
@@ -9,7 +9,7 @@ import { ProductWarehouseStock, StockLog } from '@/types/api/inventory/product';
|
|||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
import { FileDown } from 'lucide-react';
|
import { FileDown } from 'lucide-react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
const stockLogTableColumns: (warehouseName: string) => ColumnDef<StockLog>[] = (
|
||||||
@@ -80,6 +80,23 @@ const StockLogTable = ({
|
|||||||
productWarehouse: ProductWarehouseStock;
|
productWarehouse: ProductWarehouseStock;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExportLoading, setIsExportLoading] = useState(false);
|
const [isExportLoading, setIsExportLoading] = useState(false);
|
||||||
|
const [hasBeenVisible, setHasBeenVisible] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setHasBeenVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (containerRef.current) observer.observe(containerRef.current);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
@@ -108,7 +125,9 @@ const StockLogTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
const { data: stockLogsResponse, isLoading: isLoadingStockLogs } = useSWR(
|
||||||
`${StockLogApi.basePath}${getTableFilterQueryString()}`,
|
hasBeenVisible
|
||||||
|
? `${StockLogApi.basePath}${getTableFilterQueryString()}`
|
||||||
|
: null,
|
||||||
StockLogApi.getAllFetcher
|
StockLogApi.getAllFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,46 +136,48 @@ const StockLogTable = ({
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div ref={containerRef}>
|
||||||
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
<Card
|
||||||
collapsible
|
title={`Informasi Stock Produk - ${productWarehouse.warehouse_name}`}
|
||||||
variant='bordered'
|
collapsible
|
||||||
className={{
|
variant='bordered'
|
||||||
wrapper: 'w-full',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='flex justify-end px-6 pt-4'>
|
|
||||||
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
|
||||||
<FileDown size={16} />
|
|
||||||
Export Excel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Table<StockLog>
|
|
||||||
data={stockLogs}
|
|
||||||
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
|
||||||
page={tableFilterState.page ?? 0}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
onPageChange={setPage}
|
|
||||||
onPageSizeChange={setPageSize}
|
|
||||||
isLoading={isLoadingStockLogs}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(stockLogsResponse)
|
|
||||||
? stockLogsResponse.meta?.total_results
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'mt-4 mb-0',
|
wrapper: 'w-full',
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Card>
|
<div className='flex justify-end px-6 pt-4'>
|
||||||
|
<Button onClick={handleExportExcel} isLoading={isExportLoading}>
|
||||||
|
<FileDown size={16} />
|
||||||
|
Export Excel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table<StockLog>
|
||||||
|
data={stockLogs}
|
||||||
|
columns={stockLogTableColumns(productWarehouse.warehouse_name)}
|
||||||
|
page={tableFilterState.page ?? 0}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
isLoading={isLoadingStockLogs}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(stockLogsResponse)
|
||||||
|
? stockLogsResponse.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
containerClassName: 'mt-4 mb-0',
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
@@ -113,6 +113,7 @@ const CATEGORY_LABELS: { [key: string]: string } = {
|
|||||||
pullet_close: 'Pullet Close',
|
pullet_close: 'Pullet Close',
|
||||||
produksi_open: 'Produksi Open',
|
produksi_open: 'Produksi Open',
|
||||||
produksi_close: 'Produksi Close',
|
produksi_close: 'Produksi Close',
|
||||||
|
empty_kandang: 'Kandang Kosong',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
const TIME_TYPE_ORDER = ['Umum', 'Pagi', 'Siang', 'Sore', 'Malam'];
|
||||||
|
|||||||
Reference in New Issue
Block a user