Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu

This commit is contained in:
rstubryan
2026-01-15 11:48:17 +07:00
17 changed files with 425 additions and 224 deletions
+2 -2
View File
@@ -325,7 +325,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
}; };
const useSelect = <T,>( const useSelect = <T,>(
basePath: string, basePath: string | null,
valueKey: keyof T | string, valueKey: keyof T | string,
labelKey: keyof T | string, labelKey: keyof T | string,
searchKey: string = 'search', searchKey: string = 'search',
@@ -354,7 +354,7 @@ const useSelect = <T,>(
[limitKey]: String(limit), [limitKey]: String(limit),
}).toString(); }).toString();
return `${basePath}?${qs}`; return basePath ? `${basePath}?${qs}` : null;
}; };
const { const {
@@ -163,6 +163,7 @@ const ClosingsTable = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>( const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
@@ -228,6 +229,7 @@ const ClosingsTable = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6', wrapper: 'col-span-12 sm:col-span-6',
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { isResponseError } from '@/lib/api-helper';
import { InventoryAdjustmentApi } from '@/services/api/inventory'; import { InventoryAdjustmentApi } from '@/services/api/inventory';
import { import {
CreateInventoryAdjustmentPayload, CreateInventoryAdjustmentPayload,
@@ -22,12 +22,18 @@ import {
} from '@/services/api/master-data'; } from '@/services/api/master-data';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import { RadioGroup } from '@/components/input/RadioInput'; import { RadioGroup } from '@/components/input/RadioInput';
import TextArea from '@/components/input/TextArea'; import TextArea from '@/components/input/TextArea';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { Product } from '@/types/api/master-data/product';
import { Warehouse } from '@/types/api/master-data/warehouse';
interface InventoryAdjustmentFormProps { interface InventoryAdjustmentFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({
InventoryAdjustmentFormErrorMessage, InventoryAdjustmentFormErrorMessage,
setInventoryAdjustmentFormErrorMessage, setInventoryAdjustmentFormErrorMessage,
] = useState(''); ] = useState('');
const [selectedProductCategories, setSelectedProductCategories] =
useState('');
const [disabledProduct, setDisabledProduct] = useState(true); const [disabledProduct, setDisabledProduct] = useState(true);
const [optionsProduct, setOptionsProduct] = useState<OptionType[]>([]);
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok'); const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
// Submit Handler // Submit Handler
@@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({
}); });
// Fetch Data // Fetch Data
const productCategoriesUrl = `${ const {
ProductCategoryApi.basePath setInputValue: setProductCategoryInputValue,
}?${new URLSearchParams({ options: productCategoryOptions,
search: '', isLoadingOptions: isLoadingProductCategoryOptions,
}).toString()}`; loadMore: loadMoreProductCategories,
const { data: productCategories, isLoading: isLoadingProductCategories } = } = useSelect<ProductCategory>(ProductCategoryApi.basePath, 'id', 'name');
useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher);
const productUrl = `${ProductApi.basePath}?${new URLSearchParams({ const {
search: '', setInputValue: setProductInputValue,
product_category_id: selectedProductCategories, options: productOptions,
}).toString()}`; isLoadingOptions: isLoadingProductOptions,
const { data: products, isLoading: isLoadingProducts } = useSWR( loadMore: loadMoreProducts,
productUrl, } = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
ProductApi.getAllFetcher product_category_id: formik.values.product_category_id
); ? String(formik.values.product_category_id)
: '',
});
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ const {
search: '', setInputValue: setWarehouseInputValue,
limit: '100', options: warehouseOptions,
}).toString()}`; isLoadingOptions: isLoadingWarehouseOptions,
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR( loadMore: loadMoreWarehouses,
warehouseUrl, } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
WarehouseApi.getAllFetcher
);
// Map Data to Options
const optionsProductCategory = isResponseSuccess(productCategories)
? productCategories?.data.map((productCategory) => ({
value: productCategory.id,
label: productCategory.name,
}))
: [];
const optionsWarehouse = isResponseSuccess(warehouses)
? warehouses?.data.map((warehouse) => ({
value: warehouse.id,
label: warehouse.name,
}))
: [];
// Options Handler // Options Handler
const productCategoryChangeHandler = ( const productCategoryChangeHandler = (
@@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({
formik.setFieldValue('product_category', val); formik.setFieldValue('product_category', val);
setSelectedProductCategories((val as OptionType)?.value as string);
const disabled = (val as OptionType)?.value == null; const disabled = (val as OptionType)?.value == null;
setDisabledProduct(disabled); setDisabledProduct(disabled);
formik.setFieldValue('product_id', 0); formik.setFieldValue('product_id', 0);
@@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({
// Effect // Effect
useEffect(() => { useEffect(() => {
if (initialValues?.product_warehouse?.product?.id) { if (initialValues?.product_warehouse?.product?.id) {
setSelectedProductCategories(
String(initialValues.product_warehouse.product.id)
);
setDisabledProduct(false); setDisabledProduct(false);
formik.setFieldValue( formik.setFieldValue(
'product_id', 'product_id',
@@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({
); );
formik.setFieldValue('note', initialValues.note); formik.setFieldValue('note', initialValues.note);
} }
}, [ }, [formik, initialValues, setQuantityLabel, setDisabledProduct]);
formik,
initialValues,
setQuantityLabel,
setDisabledProduct,
setSelectedProductCategories,
]);
useEffect(() => { useEffect(() => {
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues); formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
}, [formikSetValues, formikInitialValues]); }, [formikSetValues, formikInitialValues]);
useEffect(() => {
if (isResponseSuccess(products)) {
const options = products.data.map((p) => ({
value: p.id,
label: p.name,
}));
setOptionsProduct(options);
}
}, [products]);
// Utils Function // Utils Function
const formatNumber = (value: string) => { const formatNumber = (value: string) => {
@@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({
label='Kategori Produk' label='Kategori Produk'
value={formik.values.product_category as OptionType} value={formik.values.product_category as OptionType}
onChange={productCategoryChangeHandler} onChange={productCategoryChangeHandler}
onInputChange={setSelectedProductCategories} onInputChange={setProductCategoryInputValue}
options={optionsProductCategory} options={productCategoryOptions}
isLoading={isLoadingProductCategories} onMenuScrollToBottom={loadMoreProductCategories}
isLoading={isLoadingProductCategoryOptions}
isError={ isError={
formik.touched.product_category && formik.touched.product_category &&
Boolean(formik.errors.product_category) Boolean(formik.errors.product_category)
@@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({
label='Produk' label='Produk'
value={formik.values.product as OptionType} value={formik.values.product as OptionType}
onChange={productChangeHandler} onChange={productChangeHandler}
options={optionsProduct} onInputChange={setProductInputValue}
isLoading={isLoadingProducts} options={productOptions}
onMenuScrollToBottom={loadMoreProducts}
isLoading={isLoadingProductOptions}
isError={formik.touched.product && Boolean(formik.errors.product)} isError={formik.touched.product && Boolean(formik.errors.product)}
errorMessage={formik.errors.product as string} errorMessage={formik.errors.product as string}
isDisabled={type === 'detail' || disabledProduct} isDisabled={type === 'detail' || disabledProduct}
@@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({
label='Warehouse' label='Warehouse'
value={formik.values.warehouse as OptionType} value={formik.values.warehouse as OptionType}
onChange={warehouseChangeHandler} onChange={warehouseChangeHandler}
options={optionsWarehouse} onInputChange={setWarehouseInputValue}
isLoading={isLoadingWarehouses} options={warehouseOptions}
onMenuScrollToBottom={loadMoreWarehouses}
isLoading={isLoadingWarehouseOptions}
isError={ isError={
formik.touched.warehouse && Boolean(formik.errors.warehouse) formik.touched.warehouse && Boolean(formik.errors.warehouse)
} }
@@ -38,6 +38,8 @@ import Card from '@/components/Card';
import { S3_PUBLIC_BASE_URL } from '@/config/constant'; import { S3_PUBLIC_BASE_URL } from '@/config/constant';
import { getUniqueFormikErrors } from '@/lib/formik-helper'; import { getUniqueFormikErrors } from '@/lib/formik-helper';
import AlertErrorList from '@/components/helper/form/FormErrors'; import AlertErrorList from '@/components/helper/form/FormErrors';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
interface MovementFormProps { interface MovementFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState(''); const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
const [
productWarehouseSelectInputValue,
setProductWarehouseSelectInputValue,
] = useState('');
const [selectedProducts, setSelectedProducts] = useState<number[]>([]); const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]); const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
const [formErrorList, setFormErrorList] = useState<string[]>([]); const [formErrorList, setFormErrorList] = useState<string[]>([]);
@@ -93,10 +91,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
// ===== USE SELECT HOOKS ===== // ===== USE SELECT HOOKS =====
const { const {
inputValue: warehouseSelectInputValue,
setInputValue: setWarehouseSelectInputValue, setInputValue: setWarehouseSelectInputValue,
isLoadingOptions: isLoadingWarehouses, isLoadingOptions: isLoadingWarehouses,
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search'); loadMore: loadMoreWarehouses,
rawData: warehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
// ===== SELECT INPUT DATA ===== // ===== SELECT INPUT DATA =====
const { const {
@@ -107,12 +106,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
category: 'BOP', category: 'BOP',
}); });
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
const { data: warehouses } = useSWR(
warehousesUrl,
WarehouseApi.getAllFetcher
);
// ===== DATA PROCESSING ===== // ===== DATA PROCESSING =====
const warehouseStockMap = useMemo(() => { const warehouseStockMap = useMemo(() => {
if (!isResponseSuccess(allProductWarehouses)) return new Map(); if (!isResponseSuccess(allProductWarehouses)) return new Map();
@@ -269,25 +262,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}); });
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) ===== // ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
const getProductWarehousesUrl = useCallback(() => { const {
const productWarehouseParams = new URLSearchParams({ setInputValue: setProductWarehouseSelectInputValue,
search: productWarehouseSelectInputValue, isLoadingOptions: isLoadingProductWarehouses,
}); loadMore: loadMoreProductWarehouses,
if (formik.values.source_warehouse_id) { rawData: productWarehouses,
productWarehouseParams.append( } = useSelect<ProductWarehouse>(
'warehouse_id', formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null,
formik.values.source_warehouse_id.toString() 'id',
); 'name',
'search',
{
warehouse_id: formik.values.source_warehouse_id
? formik.values.source_warehouse_id.toString()
: '',
} }
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`; );
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
const productWarehousesUrl = getProductWarehousesUrl();
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
useSWR(
formik.values.source_warehouse_id ? productWarehousesUrl : null,
ProductWarehouseApi.getAllFetcher
);
const productWarehouseOptions = isResponseSuccess(productWarehouses) const productWarehouseOptions = isResponseSuccess(productWarehouses)
? productWarehouses?.data.map((pw) => ({ ? productWarehouses?.data.map((pw) => ({
@@ -1006,6 +996,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}} }}
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
isError={ isError={
formik.touched.source_warehouse_id && formik.touched.source_warehouse_id &&
@@ -1104,6 +1095,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
options={warehouseOptions} options={warehouseOptions}
onInputChange={setWarehouseSelectInputValue} onInputChange={setWarehouseSelectInputValue}
isLoading={isLoadingWarehouses} isLoading={isLoadingWarehouses}
onMenuScrollToBottom={loadMoreWarehouses}
isError={ isError={
formik.touched.destination_warehouse_id && formik.touched.destination_warehouse_id &&
Boolean(formik.errors.destination_warehouse_id) Boolean(formik.errors.destination_warehouse_id)
@@ -1263,6 +1255,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
}} }}
options={productWarehouseOptions} options={productWarehouseOptions}
onInputChange={setProductWarehouseSelectInputValue} onInputChange={setProductWarehouseSelectInputValue}
onMenuScrollToBottom={loadMoreProductWarehouses}
isLoading={isLoadingProductWarehouses} isLoading={isLoadingProductWarehouses}
isDisabled={ isDisabled={
type === 'detail' || type === 'detail' ||
@@ -215,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama kandang'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -229,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama nonstock'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
type ProductFormSchemaType = { type ProductFormSchemaType = {
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
uom?: { uom?: {
value: number; value: number;
label: string; label: string;
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
} | null; } | null;
product_category_id: number; product_category_id: number;
product_price: number | string; product_price: number | string;
selling_price: number | string; selling_price?: number | string;
tax: number | string; tax?: number | string;
expiry_period: number | string; expiry_period?: number | string;
supplier_ids: number[]; suppliers: {
supplier: {
value: number;
label: string;
} | null;
price: number;
}[];
flags: string[]; flags: string[];
}; };
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
Yup.object({ Yup.object({
name: Yup.string().required('Nama wajib diisi!'), name: Yup.string().required('Nama wajib diisi!'),
brand: Yup.string().required('Merek wajib diisi!'), brand: Yup.string().required('Merek wajib diisi!'),
sku: Yup.string().required('SKU wajib diisi!'), sku: Yup.string(),
uom: Yup.object({ uom: Yup.object({
value: Yup.number() value: Yup.number()
@@ -58,23 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
.min(1, 'Harga produk tidak boleh kurang dari 1!'), .min(1, 'Harga produk tidak boleh kurang dari 1!'),
selling_price: Yup.number() selling_price: Yup.number()
.required('Harga jual wajib diisi!') .typeError('Harga hanya boleh angka!')
.typeError('Harga jual wajib diisi!')
.min(1, 'Harga jual tidak boleh kurang dari 1!'), .min(1, 'Harga jual tidak boleh kurang dari 1!'),
tax: Yup.number() tax: Yup.number()
.required('Pajak wajib diisi!') .typeError('Pajak hanya boleh angka!')
.typeError('Pajak wajib diisi!')
.min(0, 'Pajak tidak boleh kurang dari 0!') .min(0, 'Pajak tidak boleh kurang dari 0!')
.max(100, 'Pajak tidak boleh lebih dari 100%!'), .max(100, 'Pajak tidak boleh lebih dari 100%!'),
expiry_period: Yup.number() expiry_period: Yup.number()
.required('Periode kadaluarsa wajib diisi!') .typeError('Periode kadaluarsa hanya boleh angka!')
.typeError('Periode kadaluarsa wajib diisi!')
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'), .min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
supplier_ids: Yup.array() suppliers: Yup.array()
.of(Yup.number().required().typeError('Supplier tidak valid!')) .of(
Yup.object({
supplier: Yup.object({
value: Yup.number()
.min(1, 'Supplier wajib dipilih!')
.required('Supplier wajib dipilih!')
.typeError('Supplier wajib dipilih!'),
label: Yup.string().required('Supplier wajib dipilih!'),
}).required('Supplier wajib dipilih!'),
price: Yup.number()
.min(1, 'Harga tidak boleh kurang dari 1!')
.required('Harga wajib diisi!')
.typeError('Harga wajib diisi!'),
})
)
.min(1, 'Minimal harus ada 1 supplier!') .min(1, 'Minimal harus ada 1 supplier!')
.required('Supplier wajib diisi!'), .required('Supplier wajib diisi!'),
@@ -41,6 +41,8 @@ import { cn } from '@/lib/helper';
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant'; import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList'; import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
import { Supplier } from '@/types/api/master-data/supplier'; import { Supplier } from '@/types/api/master-data/supplier';
import Card from '@/components/Card';
import { removeArrayItemAndSync } from '@/lib/utils/formik';
interface ProductFormProps { interface ProductFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -101,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
selling_price: initialValues?.selling_price ?? '', selling_price: initialValues?.selling_price ?? '',
tax: initialValues?.tax ?? '', tax: initialValues?.tax ?? '',
expiry_period: initialValues?.expiry_period ?? '', expiry_period: initialValues?.expiry_period ?? '',
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [], suppliers: initialValues?.suppliers
? initialValues.suppliers.map((supplier) => ({
supplier: {
value: supplier.id,
label: supplier.name,
},
price: supplier.price,
}))
: [],
flags: initialValues?.flags ?? [], flags: initialValues?.flags ?? [],
}), }),
[initialValues] [initialValues]
@@ -120,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
uom_id: values.uom_id, uom_id: values.uom_id,
product_category_id: values.product_category_id, product_category_id: values.product_category_id,
product_price: parseInt(values.product_price.toString()) || 0, product_price: parseInt(values.product_price.toString()) || 0,
selling_price: parseInt(values.selling_price.toString()) || 0, selling_price: values.selling_price
tax: parseInt(values.tax.toString()) || 0, ? parseInt(values.selling_price.toString()) || 0
expiry_period: parseInt(values.expiry_period.toString()) || 0, : undefined,
supplier_ids: values.supplier_ids.filter( tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
(id): id is number => typeof id === 'number' expiry_period: values.expiry_period
), ? parseInt(values.expiry_period.toString()) || 0
: undefined,
suppliers: values.suppliers.map((s) => ({
supplier_id: s.supplier?.value as number,
price: parseInt(s.price.toString()) || 0,
})),
flags: values.flags.filter((f): f is string => typeof f === 'string'), flags: values.flags.filter((f): f is string => typeof f === 'string'),
}; };
switch (type) { switch (type) {
@@ -179,13 +194,29 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
category: 'SAPRONAK', category: 'SAPRONAK',
}); });
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const filteredSupplierOptions = useMemo(() => {
const arr = Array.isArray(val) ? val : val ? [val] : []; return supplierOptions.filter((opt) => {
formik.setFieldTouched('supplier_ids', true); return !formik.values.suppliers.some(
formik.setFieldValue( (s) => s.supplier?.value === opt.value
'supplier_ids', );
arr.map((v) => (v as OptionType).value) });
); }, [supplierOptions, formik.values.suppliers]);
const addSupplierHandler = () => {
formik.setFieldValue('suppliers', [
...formik.values.suppliers,
{
supplier_id: '',
price: formik.values.product_price,
},
]);
};
const deleteSupplierItemHandler = (idx: number) => {
const path = 'suppliers';
// trims values, errors, and touched at idx
removeArrayItemAndSync(formik, path, idx);
}; };
const deleteProductClickHandler = () => { const deleteProductClickHandler = () => {
@@ -201,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
router.push('/master-data/product'); router.push('/master-data/product');
}; };
const isSupplierRepeaterError = (
column: 'supplier' | 'price',
supplierIdx: number
) => {
return (
formik.touched.suppliers?.[supplierIdx]?.[column] &&
Boolean(
formik.errors.suppliers?.[supplierIdx] instanceof Object &&
formik.errors.suppliers?.[supplierIdx]?.[column]
)
);
};
useEffect(() => { useEffect(() => {
formikSetValues(formikInitialValues); formikSetValues(formikInitialValues);
}, [formikSetValues, formikInitialValues]); }, [formikSetValues, formikInitialValues]);
@@ -271,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<TextInput <TextInput
required
label='SKU' label='SKU'
name='sku' name='sku'
placeholder='Masukkan SKU...' placeholder='Masukkan SKU...'
@@ -344,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<NumberInput <NumberInput
required
label='Harga Jual' label='Harga Jual'
name='selling_price' name='selling_price'
placeholder='Masukkan harga jual...' placeholder='Masukkan harga jual...'
@@ -366,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
</div> </div>
<div className='grid sm:grid-cols-2 gap-4'> <div className='grid sm:grid-cols-2 gap-4'>
<NumberInput <NumberInput
required
label='Pajak (%)' label='Pajak (%)'
name='tax' name='tax'
placeholder='Masukkan pajak...' placeholder='Masukkan pajak...'
@@ -383,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
<NumberInput <NumberInput
required
label='Periode Kadaluarsa (hari)' label='Periode Kadaluarsa (hari)'
name='expiry_period' name='expiry_period'
placeholder='Masukkan periode kadaluarsa...' placeholder='Masukkan periode kadaluarsa...'
@@ -403,28 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
readOnly={type === 'detail'} readOnly={type === 'detail'}
/> />
</div> </div>
<div className='grid sm:grid-cols-2 gap-4'> <div className='grid sm:grid-cols-1 gap-4'>
<SelectInput
required
label='Supplier'
placeholder='Pilih supplier...'
isMulti
value={supplierOptions.filter((opt) =>
(formik.values.supplier_ids || []).includes(opt.value)
)}
onChange={supplierChangeHandler}
options={supplierOptions}
onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers}
isError={
formik.touched.supplier_ids &&
Boolean(formik.errors.supplier_ids)
}
errorMessage={formik.errors.supplier_ids as string}
isDisabled={type === 'detail'}
isClearable
/>
<SelectInput <SelectInput
required required
label='Flags' label='Flags'
@@ -447,6 +466,126 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
isClearable isClearable
/> />
</div> </div>
<div className='grid sm:grid-cols-1 gap-4'>
{type !== 'detail' && formik.values.suppliers.length === 0 && (
<Button
type='button'
color='success'
onClick={addSupplierHandler}
className='w-fit mx-auto'
>
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah
Supplier
</Button>
)}
{formik.values.suppliers.length > 0 && (
<Card
className={{
wrapper: 'w-full',
body: 'p-4 shadow',
}}
>
<div className='mb-4 text-center'>
<h4 className='font-bold text-xl'>Supplier</h4>
</div>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Supplier
</th>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Harga
</th>
<th>Aksi</th>
</tr>
</thead>
<tbody>
{formik.values.suppliers.map((supplier, idx) => (
<tr key={idx}>
<td className='p-2 w-full max-w-1/2'>
<SelectInput
placeholder='Pilih Supplier'
options={filteredSupplierOptions}
onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers}
value={formik.values.suppliers[idx].supplier}
onChange={(val) => {
formik.setFieldValue(
`suppliers.${idx}.supplier`,
val
);
}}
isError={isSupplierRepeaterError(
'supplier',
idx
)}
isClearable
className={{
wrapper: 'min-w-48 w-full',
}}
/>
</td>
<td className='p-2 w-full max-w-1/2'>
<NumberInput
required
name={`suppliers.${idx}.price`}
placeholder='Masukkan harga...'
value={formik.values.suppliers[idx].price}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
decimalScale={2}
allowNegative={false}
thousandSeparator=','
decimalSeparator='.'
inputPrefix='Rp '
isError={isSupplierRepeaterError('price', idx)}
readOnly={type === 'detail'}
className={{
wrapper: 'min-w-48 w-full',
}}
/>
</td>
{type !== 'detail' && (
<td>
<Button
type='button'
color='error'
onClick={() => deleteSupplierItemHandler(idx)}
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
/>
</Button>
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
<div className='w-full flex flex-row justify-center'>
<Button
type='button'
color='success'
onClick={addSupplierHandler}
>
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
Tambah Supplier
</Button>
</div>
</Card>
)}
</div>
</div> </div>
<div className='flex flex-row justify-between gap-2 flex-wrap'> <div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && ( {type !== 'add' && (
@@ -330,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
required required
label='Nama' label='Nama'
name='name' name='name'
placeholder='Masukkan nama lokasi' placeholder='Masukkan nama warehouse'
value={formik.values.name} value={formik.values.name}
onChange={formik.handleChange} onChange={formik.handleChange}
onBlur={formik.handleBlur} onBlur={formik.handleBlur}
@@ -87,6 +87,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setAreaInputValue, setInputValue: setAreaInputValue,
options: areaOptions, options: areaOptions,
isLoadingOptions: isLoadingAreaOptions, isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name'); } = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -101,6 +102,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name'); } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -118,6 +120,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setWarehouseInputValue, setInputValue: setWarehouseInputValue,
options: warehouseOptions, options: warehouseOptions,
isLoadingOptions: isLoadingWarehouseOptions, isLoadingOptions: isLoadingWarehouseOptions,
loadMore: loadMoreWarehouses,
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name'); } = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => { const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -135,6 +138,7 @@ const DailyMarketingReportContent = () => {
setInputValue: setCustomerInputValue, setInputValue: setCustomerInputValue,
options: customerOptions, options: customerOptions,
isLoadingOptions: isLoadingCustomerOptions, isLoadingOptions: isLoadingCustomerOptions,
loadMore: loadMoreCustomers,
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name'); } = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
const customerChangeHandler = (val: OptionType | OptionType[] | null) => { const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -298,6 +302,7 @@ const DailyMarketingReportContent = () => {
value={selectedArea} value={selectedArea}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue} onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -312,6 +317,7 @@ const DailyMarketingReportContent = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -326,6 +332,7 @@ const DailyMarketingReportContent = () => {
value={selectedWarehouse} value={selectedWarehouse}
onChange={warehouseChangeHandler} onChange={warehouseChangeHandler}
onInputChange={setWarehouseInputValue} onInputChange={setWarehouseInputValue}
onMenuScrollToBottom={loadMoreWarehouses}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -340,6 +347,7 @@ const DailyMarketingReportContent = () => {
value={selectedCustomer} value={selectedCustomer}
onChange={customerChangeHandler} onChange={customerChangeHandler}
onInputChange={setCustomerInputValue} onInputChange={setCustomerInputValue}
onMenuScrollToBottom={loadMoreCustomers}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { generateReportExpensePDF } from './pdf/ReportExpenseExport'; import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import {
KandangApi,
LocationApi,
NonstockApi,
SupplierApi,
} from '@/services/api/master-data';
import { Supplier } from '@/types/api/master-data/supplier';
import { Kandang } from '@/types/api/master-data/kandang';
import { Nonstock } from '@/types/api/master-data/nonstock';
const ReportExpenseTable = () => { const ReportExpenseTable = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
@@ -64,16 +73,33 @@ const ReportExpenseTable = () => {
}); });
// ===== SELECT OPTIONS ===== // ===== SELECT OPTIONS =====
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } = const {
useSelect(`/master-data/locations`, 'id', 'name'); setInputValue: setLocationInputValue,
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } = options: locationOptions,
useSelect(`/master-data/suppliers`, 'id', 'name'); isLoadingOptions: isLoadingLocationOptions,
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } = loadMore: loadMoreLocations,
useSelect(`/master-data/kandangs`, 'id', 'name', '', { } = useSelect<Location>(LocationApi.basePath, 'id', 'name');
location_id: filterState.location_id,
}); const {
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } = setInputValue: setSupplierInputValue,
useSelect(`/master-data/nonstocks`, 'id', 'name'); options: supplierOptions,
isLoadingOptions: isLoadingSupplierOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const {
setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangOptions,
loadMore: loadMoreKandangs,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
loadMore: loadMoreNonstocks,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
const categoryOptions = useMemo( const categoryOptions = useMemo(
() => [ () => [
@@ -86,31 +112,31 @@ const ReportExpenseTable = () => {
// Mendapatkan value option select dari filter state // Mendapatkan value option select dari filter state
const selectedLocation = useMemo( const selectedLocation = useMemo(
() => () =>
optionsLocation.find( locationOptions.find(
(opt) => String(opt.value) === filterState.location_id (opt) => String(opt.value) === filterState.location_id
) || null, ) || null,
[optionsLocation, filterState.location_id] [locationOptions, filterState.location_id]
); );
const selectedSupplier = useMemo( const selectedSupplier = useMemo(
() => () =>
optionsSupplier.find( supplierOptions.find(
(opt) => String(opt.value) === filterState.supplier_id (opt) => String(opt.value) === filterState.supplier_id
) || null, ) || null,
[optionsSupplier, filterState.supplier_id] [supplierOptions, filterState.supplier_id]
); );
const selectedKandang = useMemo( const selectedKandang = useMemo(
() => () =>
optionsKandang.find( kandangOptions.find(
(opt) => String(opt.value) === filterState.kandang_id (opt) => String(opt.value) === filterState.kandang_id
) || null, ) || null,
[optionsKandang, filterState.kandang_id] [kandangOptions, filterState.kandang_id]
); );
const selectedNonstock = useMemo( const selectedNonstock = useMemo(
() => () =>
optionsNonstock.find( nonstockOptions.find(
(opt) => String(opt.value) === filterState.nonstock_id (opt) => String(opt.value) === filterState.nonstock_id
) || null, ) || null,
[optionsNonstock, filterState.nonstock_id] [nonstockOptions, filterState.nonstock_id]
); );
const selectedCategory = useMemo( const selectedCategory = useMemo(
() => () =>
@@ -756,38 +782,46 @@ const ReportExpenseTable = () => {
<SelectInput <SelectInput
isClearable isClearable
label='Lokasi' label='Lokasi'
options={optionsLocation} options={locationOptions}
isLoading={isLoadingLocation} isLoading={isLoadingLocationOptions}
placeholder='Lokasi' placeholder='Lokasi'
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Kandang' label='Kandang'
options={optionsKandang} options={kandangOptions}
isLoading={isLoadingKandang} isLoading={isLoadingKandangOptions}
placeholder='Kandang' placeholder='Kandang'
value={selectedKandang} value={selectedKandang}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Supplier' label='Supplier'
options={optionsSupplier} options={supplierOptions}
isLoading={isLoadingSupplier} isLoading={isLoadingSupplierOptions}
placeholder='Supplier' placeholder='Supplier'
value={selectedSupplier} value={selectedSupplier}
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
/> />
<SelectInput <SelectInput
isClearable isClearable
label='Produk' label='Produk'
options={optionsNonstock} options={nonstockOptions}
isLoading={isLoadingNonstock} isLoading={isLoadingNonstockOptions}
placeholder='Produk' placeholder='Produk'
value={selectedNonstock} value={selectedNonstock}
onChange={nonstockChangeHandler} onChange={nonstockChangeHandler}
onInputChange={setNonstockInputValue}
onMenuScrollToBottom={loadMoreNonstocks}
/> />
<SelectInput <SelectInput
isClearable isClearable
@@ -57,6 +57,7 @@ const CustomerPaymentTab = () => {
const { const {
options: customerOptions, options: customerOptions,
setInputValue: setCustomerInputValue,
isLoadingOptions: isLoadingCustomers, isLoadingOptions: isLoadingCustomers,
loadMore: loadMoreCustomers, loadMore: loadMoreCustomers,
hasMore: hasMoreCustomers, hasMore: hasMoreCustomers,
@@ -65,6 +66,7 @@ const CustomerPaymentTab = () => {
// TODO: Uncomment when BE is ready // TODO: Uncomment when BE is ready
const { const {
options: salesOptions, options: salesOptions,
setInputValue: setSalesInputValue,
isLoadingOptions: isLoadingSales, isLoadingOptions: isLoadingSales,
loadMore: loadMoreSales, loadMore: loadMoreSales,
hasMore: hasMoreSales, hasMore: hasMoreSales,
@@ -653,6 +655,7 @@ const CustomerPaymentTab = () => {
Array.isArray(val) ? val : val ? [val] : [] Array.isArray(val) ? val : val ? [val] : []
); );
}} }}
onInputChange={setCustomerInputValue}
isLoading={isLoadingCustomers} isLoading={isLoadingCustomers}
isClearable isClearable
onMenuScrollToBottom={loadMoreCustomers} onMenuScrollToBottom={loadMoreCustomers}
@@ -670,6 +673,7 @@ const CustomerPaymentTab = () => {
onChange={(val) => { onChange={(val) => {
setFilterSales(Array.isArray(val) ? val : val ? [val] : []); setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
}} }}
onInputChange={setSalesInputValue}
isLoading={isLoadingSales} isLoading={isLoadingSales}
isClearable isClearable
onMenuScrollToBottom={loadMoreSales} onMenuScrollToBottom={loadMoreSales}
@@ -33,6 +33,7 @@ import {
} from '@/components/pages/report/finance/filter/DebtSupplierFilter'; } from '@/components/pages/report/finance/filter/DebtSupplierFilter';
import { getFilledFormikValuesCount } from '@/lib/formik-helper'; import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import ButtonFilter from '@/components/helper/ButtonFilter'; import ButtonFilter from '@/components/helper/ButtonFilter';
import { Supplier } from '@/types/api/master-data/supplier';
const DebtSupplierTab = () => { const DebtSupplierTab = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
@@ -51,10 +52,12 @@ const DebtSupplierTab = () => {
const filterModal = useModal(); const filterModal = useModal();
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = const {
useSelect(SupplierApi.basePath, 'id', 'name', '', { setInputValue: setSupplierInputValue,
limit: 'limit', options: supplierOptions,
}); isLoadingOptions: isLoadingSupplierOptions,
loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const dataTypeOptions = useMemo( const dataTypeOptions = useMemo(
() => [ () => [
@@ -610,7 +613,9 @@ const DebtSupplierTab = () => {
Array.isArray(val) ? val : val ? [val] : null Array.isArray(val) ? val : val ? [val] : null
); );
}} }}
isLoading={isLoadingSuppliers} onInputChange={setSupplierInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSupplierOptions}
isClearable isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={ isError={
@@ -62,6 +62,7 @@ const ProductionResultContent = () => {
setInputValue: setAreaInputValue, setInputValue: setAreaInputValue,
options: areaOptions, options: areaOptions,
isLoadingOptions: isLoadingAreaOptions, isLoadingOptions: isLoadingAreaOptions,
loadMore: loadMoreAreas,
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name'); } = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
@@ -78,6 +79,7 @@ const ProductionResultContent = () => {
setInputValue: setLocationInputValue, setInputValue: setLocationInputValue,
options: locationOptions, options: locationOptions,
isLoadingOptions: isLoadingLocationOptions, isLoadingOptions: isLoadingLocationOptions,
loadMore: loadMoreLocations,
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', { } = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '', area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
}); });
@@ -94,6 +96,7 @@ const ProductionResultContent = () => {
setInputValue: setProjectFlockInputValue, setInputValue: setProjectFlockInputValue,
options: projectFlockOptions, options: projectFlockOptions,
isLoadingOptions: isLoadingProjectFlockOptions, isLoadingOptions: isLoadingProjectFlockOptions,
loadMore: loadMoreProjectFlocks,
} = useSelect<BaseKandang>( } = useSelect<BaseKandang>(
ProjectFlockApi.basePath, ProjectFlockApi.basePath,
'id', 'id',
@@ -120,6 +123,7 @@ const ProductionResultContent = () => {
setInputValue: setProjectFlockKandangInputValue, setInputValue: setProjectFlockKandangInputValue,
options: projectFlockKandangOptions, options: projectFlockKandangOptions,
isLoadingOptions: isLoadingProjectFlockKandangOptions, isLoadingOptions: isLoadingProjectFlockKandangOptions,
loadMore: loadMoreProjectFlockKandangs,
} = useSelect<BaseKandang>( } = useSelect<BaseKandang>(
ProjectFlockKandangApi.basePath, ProjectFlockKandangApi.basePath,
'id', 'id',
@@ -235,6 +239,7 @@ const ProductionResultContent = () => {
value={selectedArea} value={selectedArea}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue} onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isClearable isClearable
className={{ className={{
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4', wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
@@ -251,6 +256,7 @@ const ProductionResultContent = () => {
value={selectedLocation} value={selectedLocation}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue} onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isClearable isClearable
isDisabled={!selectedArea} isDisabled={!selectedArea}
className={{ className={{
@@ -270,6 +276,7 @@ const ProductionResultContent = () => {
value={selectedProjectFlock} value={selectedProjectFlock}
onChange={projectFlockChangeHandler} onChange={projectFlockChangeHandler}
onInputChange={setProjectFlockInputValue} onInputChange={setProjectFlockInputValue}
onMenuScrollToBottom={loadMoreProjectFlocks}
isClearable isClearable
isDisabled={!selectedArea || !selectedLocation} isDisabled={!selectedArea || !selectedLocation}
className={{ className={{
@@ -289,6 +296,7 @@ const ProductionResultContent = () => {
value={selectedProjectFlockKandang} value={selectedProjectFlockKandang}
onChange={projectFlockKandangChangeHandler} onChange={projectFlockKandangChangeHandler}
onInputChange={setProjectFlockKandangInputValue} onInputChange={setProjectFlockKandangInputValue}
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
isClearable isClearable
isDisabled={!selectedProjectFlock} isDisabled={!selectedProjectFlock}
className={{ className={{
@@ -58,18 +58,26 @@ const HppPerKandangTab = () => {
}, },
}); });
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect( const {
AreaApi.basePath, setInputValue: setAreaInputValue,
'id', options: areaOptions,
'name', isLoadingOptions: isLoadingAreas,
'search' loadMore: loadMoreAreas,
); } = useSelect(AreaApi.basePath, 'id', 'name', 'search');
const { options: locationOptions, isLoadingOptions: isLoadingLocations } = const {
useSelect(LocationApi.basePath, 'id', 'name', 'search'); setInputValue: setLocationInputValue,
options: locationOptions,
isLoadingOptions: isLoadingLocations,
loadMore: loadMoreLocations,
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } = const {
useSelect(KandangApi.basePath, 'id', 'name', 'search'); setInputValue: setKandangInputValue,
options: kandangOptions,
isLoadingOptions: isLoadingKandangs,
loadMore: loadMoreKandangs,
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
const showUnrecordedOptions: OptionType[] = [ const showUnrecordedOptions: OptionType[] = [
{ value: 'false', label: 'Sembunyikan' }, { value: 'false', label: 'Sembunyikan' },
@@ -810,6 +818,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={areaChangeHandler} onChange={areaChangeHandler}
onInputChange={setAreaInputValue}
onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreas} isLoading={isLoadingAreas}
isClearable isClearable
/> />
@@ -824,6 +834,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={locationChangeHandler} onChange={locationChangeHandler}
onInputChange={setLocationInputValue}
onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocations} isLoading={isLoadingLocations}
isClearable isClearable
/> />
@@ -838,6 +850,8 @@ const HppPerKandangTab = () => {
.includes(String(opt.value)) .includes(String(opt.value))
)} )}
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
onInputChange={setKandangInputValue}
onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangs} isLoading={isLoadingKandangs}
isClearable isClearable
/> />
+16 -16
View File
@@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/dashboard/': ['lti.dashboard.list'], '/dashboard/': ['lti.dashboard.list'],
// Daily Checklist // Daily Checklist
// TODO: use real daily checklist permission name '/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'],
// '/daily-checklist/': ['lti.daily_checklist.list'], '/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'],
// '/daily-checklist/dashboard/': ['lti.daily_checklist.list'], '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
// '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'], '/daily-checklist/list-daily-checklist/detail/': [
// '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'], 'lti.daily_checklist.detail',
// '/daily-checklist/reports/': ['lti.daily_checklist.reports'], ],
// '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'], '/daily-checklist/reports/': ['lti.daily_checklist.reports'],
// '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'], '/daily-checklist/master-data/employee/': [
'/daily-checklist/dashboard/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.employee',
'/daily-checklist/daily-checklist/': ['lti.dashboard.list'], ],
'/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'], '/daily-checklist/master-data/activity/': [
'/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.activity',
'/daily-checklist/reports/': ['lti.dashboard.list'], ],
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'], '/daily-checklist/master-data/configuration/': [
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'], 'lti.daily_checklist.master_data.configuration',
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'], ],
// Production // Production
// Production - Project Flock // Production - Project Flock
+12 -9
View File
@@ -1,20 +1,20 @@
import { BaseMetadata } from '@/types/api/api-general'; import { BaseMetadata } from '@/types/api/api-general';
import { Uom } from '@/types/api/master-data/uom'; import { Uom } from '@/types/api/master-data/uom';
import { ProductCategory } from '@/types/api/master-data/product-category'; import { ProductCategory } from '@/types/api/master-data/product-category';
import { Supplier } from '@/types/api/master-data/supplier'; import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier';
export type BaseProduct = { export type BaseProduct = {
id: number; id: number;
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
product_price: number; product_price: number;
selling_price?: number; selling_price?: number;
tax?: number; tax?: number;
expiry_period: number; expiry_period?: number;
uom: Uom; uom: Uom;
product_category: ProductCategory; product_category: ProductCategory;
suppliers: Supplier[]; suppliers: (BaseSupplier & { price: number })[];
flags: string[]; flags: string[];
}; };
@@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct;
export type CreateProductPayload = { export type CreateProductPayload = {
name: string; name: string;
brand: string; brand: string;
sku: string; sku?: string;
uom_id: number; uom_id: number;
product_category_id: number; product_category_id: number;
product_price: number; product_price: number;
selling_price: number; selling_price?: number;
tax: number; tax?: number;
expiry_period: number; expiry_period?: number;
supplier_ids: number[]; suppliers: {
supplier_id: number;
price: number;
}[];
flags: string[]; flags: string[];
}; };