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

This commit is contained in:
rstubryan
2026-01-14 12:00:50 +07:00
16 changed files with 657 additions and 381 deletions
+40
View File
@@ -0,0 +1,40 @@
import Button, { ButtonProps } from '@/components/Button';
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import { Icon } from '@iconify/react';
import { FormikValues } from 'formik';
export type ButtonFilterProps = ButtonProps & {
values: FormikValues;
onClick: () => void;
};
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
return (
<Button
{...props}
onClick={onClick}
className={
getFilledFormikValuesCount(values) > 0
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
: ''
}
>
<Icon
icon='heroicons:funnel'
width={20}
height={20}
className={
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
}
/>
Filter
{getFilledFormikValuesCount(values) > 0 && (
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
{getFilledFormikValuesCount(values)}
</span>
)}
</Button>
);
};
export default ButtonFilter;
+115 -21
View File
@@ -9,15 +9,20 @@ import Select, {
SingleValue, SingleValue,
components as ReactSelectComponents, components as ReactSelectComponents,
ControlProps, ControlProps,
MenuListProps,
} from 'react-select'; } from 'react-select';
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper'; import { cn, getByPath } from '@/lib/helper';
import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite';
import { httpClientFetcher } from '@/services/http/client'; import { httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import {
import { isResponseSuccess } from '@/lib/api-helper'; BaseApiResponse,
ErrorApiResponse,
SuccessApiResponse,
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType { export interface OptionType {
value: string | number; value: string | number;
@@ -59,6 +64,7 @@ interface SelectInputBaseProps<T = OptionType> {
menuPortalTarget?: HTMLElement | null; menuPortalTarget?: HTMLElement | null;
closeMenuOnSelect?: boolean; closeMenuOnSelect?: boolean;
hideSelectedOptions?: boolean; hideSelectedOptions?: boolean;
onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined;
} }
export interface SelectInputProps<T = OptionType> export interface SelectInputProps<T = OptionType>
@@ -97,6 +103,29 @@ const CustomControl = <
); );
}; };
const CustomMenuList = <
Option,
IsMulti extends boolean,
Group extends GroupBase<Option>,
>(
props: MenuListProps<Option, IsMulti, Group>
) => {
const { children, selectProps, options } = props;
const { isLoading } = selectProps;
return (
<ReactSelectComponents.MenuList {...props}>
{children}
{options.length > 0 && isLoading && (
<div className='mt-1 px-3 py-2 rounded-md text-center text-gray-400'>
<span className='loading loading-spinner loading-md' />
</div>
)}
</ReactSelectComponents.MenuList>
);
};
const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => { const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const { const {
label, label,
@@ -126,6 +155,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
menuPortalTarget, menuPortalTarget,
closeMenuOnSelect, closeMenuOnSelect,
hideSelectedOptions, hideSelectedOptions,
onMenuScrollToBottom,
} = props; } = props;
const [internalInputValue, setInternalInputValue] = useState(''); const [internalInputValue, setInternalInputValue] = useState('');
@@ -269,6 +299,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
components={{ components={{
...components, ...components,
...(optionComponent ? { Option: optionComponent } : {}), ...(optionComponent ? { Option: optionComponent } : {}),
MenuList: CustomMenuList,
}} }}
{...(startAdornment && { {...(startAdornment && {
shouldShowAdornment, shouldShowAdornment,
@@ -282,6 +313,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
styles={{ styles={{
menuPortal: (base) => ({ ...base, zIndex: 9999 }), menuPortal: (base) => ({ ...base, zIndex: 9999 }),
}} }}
onMenuScrollToBottom={onMenuScrollToBottom}
/> />
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>} {isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
@@ -301,34 +333,96 @@ const useSelect = <T,>(
) => { ) => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const optionsUrlParams = useMemo(() => { const pageKey = 'page';
return new URLSearchParams({ const limitKey = 'limit';
const limit = params?.['limit'] ?? 10;
const getKey = (
pageIndex: number,
previousPageData?: BaseApiResponse<T[]>
) => {
// stop when backend says no more pages
if (previousPageData && isResponseSuccess(previousPageData)) {
const meta = previousPageData.meta;
if (meta && meta.page >= meta.total_pages) return null;
}
const qs = new URLSearchParams({
...(params ?? {}),
[searchKey]: inputValue ?? '', [searchKey]: inputValue ?? '',
...params, [pageKey]: String(pageIndex + 1),
[limitKey]: String(limit),
}).toString(); }).toString();
}, [inputValue, searchKey, params]);
const optionsUrl = `${basePath}?${optionsUrlParams}`; return `${basePath}?${qs}`;
};
const { data, isLoading } = useSWR(optionsUrl, async (url) => { const {
return await httpClientFetcher<BaseApiResponse<T[]>>(url); data: pages,
}); isLoading,
isValidating,
size,
setSize,
} = useSWRInfinite<BaseApiResponse<T[]>>(getKey, (url) =>
httpClientFetcher<BaseApiResponse<T[]>>(url)
);
const options = isResponseSuccess(data) const options = useMemo(() => {
? data.data.map((item) => { if (!pages) return [];
return {
value: getByPath<T, number>(item, valueKey as string), return pages.flatMap((page) =>
label: getByPath<T, string>(item, labelKey as string), isResponseSuccess(page)
}; ? page.data.map((item) => ({
}) value: getByPath<T, number>(item, valueKey as string),
: []; label: getByPath<T, string>(item, labelKey as string),
}))
: []
);
}, [pages, valueKey, labelKey]);
const lastPage = pages?.[pages.length - 1];
const hasMore =
!!lastPage &&
isResponseSuccess(lastPage) &&
!!lastPage.meta &&
lastPage.meta.page < lastPage.meta.total_pages;
const loadMore = () => {
if (!hasMore) return;
setSize(size + 1);
};
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = {
...pages?.[latestPagesIndex],
data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
[],
};
}
if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex];
}
return { return {
inputValue, inputValue,
setInputValue, setInputValue,
options, options,
isLoadingOptions: isLoading, rawData: isResponseSuccess(pages?.[latestPagesIndex])
rawData: data, ? formattedSuccessRawData
: formattedErrorRawData,
isLoadingOptions: isLoading || isValidating,
isLoadingMore: isValidating && size > 1,
hasMore,
loadMore,
}; };
}; };
@@ -23,13 +23,17 @@ import TextInput from '@/components/input/TextInput';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import TextArea from '@/components/input/TextArea'; import TextArea from '@/components/input/TextArea';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import useSWR from 'swr'; import useSWR from 'swr';
import { UserApi } from '@/services/api/user'; import { UserApi } from '@/services/api/user';
import { TYPE_OPTIONS } from '@/config/constant'; import { TYPE_OPTIONS } from '@/config/constant';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
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 { User } from '@/types/api/api-general';
interface CustomerFormProps { interface CustomerFormProps {
formType?: 'add' | 'edit' | 'detail'; formType?: 'add' | 'edit' | 'detail';
@@ -47,25 +51,15 @@ const CustomerForm = ({
// Setup State // Setup State
const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState(''); const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState('');
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [picSelectInputValue, setPicSelectInputValue] = useState('');
// Fetch Data const {
const picUrl = `${UserApi.basePath}?${new URLSearchParams({ setInputValue: setPicSelectInputValue,
search: picSelectInputValue ?? '', options: picOptions,
})}`; isLoadingOptions: isLoadingPicOptions,
loadMore: loadMorePic,
const { data: pic, isLoading: isLoadingPic } = useSWR( } = useSelect<User>(UserApi.basePath, 'id', 'name');
picUrl,
UserApi.getAllFetcher
);
// -- Options data mapping // -- Options data mapping
const picOptions = isResponseSuccess(pic)
? pic?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const typeOptions = TYPE_OPTIONS; const typeOptions = TYPE_OPTIONS;
// Handler Event // Handler Event
@@ -240,11 +234,12 @@ const CustomerForm = ({
required required
placeholder='Pilih PIC' placeholder='Pilih PIC'
label='PIC' label='PIC'
value={formik.values.pic ?? undefined} value={formik.values.pic?.value ? formik.values.pic : undefined}
onChange={picChangeHandler} onChange={picChangeHandler}
options={picOptions} options={picOptions}
onInputChange={setPicSelectInputValue} onInputChange={setPicSelectInputValue}
isLoading={isLoadingPic} onMenuScrollToBottom={loadMorePic}
isLoading={isLoadingPicOptions}
isError={formik.touched.picId && Boolean(formik.errors.picId)} isError={formik.touched.picId && Boolean(formik.errors.picId)}
errorMessage={formik.errors.picId as string} errorMessage={formik.errors.picId as string}
isDisabled={formType === 'detail'} isDisabled={formType === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -31,6 +34,7 @@ import { UserApi } from '@/services/api/user';
import NumberInput from '@/components/input/NumberInput'; import NumberInput from '@/components/input/NumberInput';
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 { User } from '@/types/api/api-general';
interface KandangFormProps { interface KandangFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -128,23 +132,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// location // location
const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const {
setInputValue: setLocationSelectInputValue,
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ options: locationOptions,
search: locationSelectInputValue ?? '', isLoadingOptions: isLoadingLocationOptions,
}).toString()}`; loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationsUrl,
LocationApi.getAllFetcher
);
const locationOptions = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
const locationChangeHandler = (val: OptionType | OptionType[] | null) => { const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('location', true); formik.setFieldTouched('location', true);
@@ -155,23 +148,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
}; };
// PIC // PIC
const [picSelectInputValue, setPicSelectInputValue] = useState(''); const {
setInputValue: setPicSelectInputValue,
const picsUrl = `${UserApi.basePath}?${new URLSearchParams({ options: picOptions,
search: picSelectInputValue ?? '', isLoadingOptions: isLoadingPicOptions,
}).toString()}`; loadMore: loadMorePics,
} = useSelect<User>(UserApi.basePath, 'id', 'name');
const { data: pics, isLoading: isLoadingPics } = useSWR(
picsUrl,
LocationApi.getAllFetcher
);
const picOptions = isResponseSuccess(pics)
? pics?.data.map((pic) => ({
value: pic.id,
label: pic.name,
}))
: [];
const picChangeHandler = (val: OptionType | OptionType[] | null) => { const picChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('pic', true); formik.setFieldTouched('pic', true);
@@ -249,7 +231,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={locationOptions} options={locationOptions}
onInputChange={setLocationSelectInputValue} onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations} onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocationOptions}
isError={ isError={
formik.touched.locationId && Boolean(formik.errors.locationId) formik.touched.locationId && Boolean(formik.errors.locationId)
} }
@@ -280,7 +263,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
onChange={picChangeHandler} onChange={picChangeHandler}
options={picOptions} options={picOptions}
onInputChange={setPicSelectInputValue} onInputChange={setPicSelectInputValue}
isLoading={isLoadingPics} onMenuScrollToBottom={loadMorePics}
isLoading={isLoadingPicOptions}
isError={formik.touched.picId && Boolean(formik.errors.picId)} isError={formik.touched.picId && Boolean(formik.errors.picId)}
errorMessage={formik.errors.picId as string} errorMessage={formik.errors.picId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -29,6 +32,7 @@ import { AreaApi, LocationApi } from '@/services/api/master-data';
import { cn } from '@/lib/helper'; import { cn } from '@/lib/helper';
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 { Area } from '@/types/api/master-data/area';
interface LocationFormProps { interface LocationFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -117,23 +121,12 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); const {
setInputValue: setAreaSelectInputValue,
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ options: areaOptions,
search: areaSelectInputValue ?? '', isLoadingOptions: isLoadingAreaOptions,
}).toString()}`; loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areasUrl,
AreaApi.getAllFetcher
);
const areaOptions = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
const areaChangeHandler = (val: OptionType | OptionType[] | null) => { const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('area', true); formik.setFieldTouched('area', true);
@@ -224,7 +217,8 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
onChange={areaChangeHandler} onChange={areaChangeHandler}
options={areaOptions} options={areaOptions}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas} onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreaOptions}
isError={formik.touched.areaId && Boolean(formik.errors.areaId)} isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
errorMessage={formik.errors.areaId as string} errorMessage={formik.errors.areaId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -31,6 +34,8 @@ import { flags } from '@/types/api/api-general';
import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant'; import { SUPPLIER_FLAG_OPTIONS } from '@/config/constant';
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 { Supplier } from '@/types/api/master-data/supplier';
import { Uom } from '@/types/api/master-data/uom';
interface NonstockFormProps { interface NonstockFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -129,23 +134,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// UOM // UOM
const [uomSelectInputValue, setUomSelectInputValue] = useState(''); const {
setInputValue: setUomSelectInputValue,
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({ options: uomOptions,
search: uomSelectInputValue ?? '', isLoadingOptions: isLoadingUomOptions,
}).toString()}`; loadMore: loadMoreUoms,
} = useSelect<Uom>(UomApi.basePath, 'id', 'name');
const { data: uoms, isLoading: isLoadingUoms } = useSWR(
uomsUrl,
UomApi.getAllFetcher
);
const uomOptions = isResponseSuccess(uoms)
? uoms?.data.map((uom) => ({
value: uom.id,
label: uom.name,
}))
: [];
const uomChangeHandler = (val: OptionType | OptionType[] | null) => { const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true); formik.setFieldTouched('uom', true);
@@ -156,25 +150,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
}; };
// supplier // supplier
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); const {
setInputValue: setSupplierSelectInputValue,
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ options: supplierOptions,
search: supplierSelectInputValue ?? '', isLoadingOptions: isLoadingSupplierOptions,
}).toString()}`; loadMore: loadMoreSuppliers,
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
suppliersUrl,
SupplierApi.getAllFetcher
);
const supplierOptions = isResponseSuccess(suppliers)
? suppliers?.data
.filter((sup) => sup.category === 'BOP')
.map((supplier) => ({
value: supplier.id,
label: supplier.name,
}))
: [];
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('suppliers', true); formik.setFieldTouched('suppliers', true);
@@ -264,7 +245,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
onChange={uomChangeHandler} onChange={uomChangeHandler}
options={uomOptions} options={uomOptions}
onInputChange={setUomSelectInputValue} onInputChange={setUomSelectInputValue}
isLoading={isLoadingUoms} isLoading={isLoadingUomOptions}
onMenuScrollToBottom={loadMoreUoms}
isError={formik.touched.uomId && Boolean(formik.errors.uomId)} isError={formik.touched.uomId && Boolean(formik.errors.uomId)}
errorMessage={formik.errors.uomId as string} errorMessage={formik.errors.uomId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -278,7 +260,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions ?? []} options={supplierOptions ?? []}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
isLoading={isLoadingSuppliers} onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSupplierOptions}
isError={ isError={
formik.touched.suppliers && Boolean(formik.errors.suppliers) formik.touched.suppliers && Boolean(formik.errors.suppliers)
} }
@@ -40,6 +40,7 @@ import {
import { cn } from '@/lib/helper'; 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';
interface ProductFormProps { interface ProductFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -145,6 +146,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
setInputValue: setUomSelectInputValue, setInputValue: setUomSelectInputValue,
options: uomOptions, options: uomOptions,
isLoadingOptions: isLoadingUoms, isLoadingOptions: isLoadingUoms,
loadMore: loadMoreUoms,
} = useSelect(UomApi.basePath, 'id', 'name'); } = useSelect(UomApi.basePath, 'id', 'name');
const uomChangeHandler = (val: OptionType | OptionType[] | null) => { const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('uom', true); formik.setFieldTouched('uom', true);
@@ -158,6 +160,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
setInputValue: setCategorySelectInputValue, setInputValue: setCategorySelectInputValue,
options: categoryOptions, options: categoryOptions,
isLoadingOptions: isLoadingCategories, isLoadingOptions: isLoadingCategories,
loadMore: loadMoreCategories,
} = useSelect(ProductCategoryApi.basePath, 'id', 'name'); } = useSelect(ProductCategoryApi.basePath, 'id', 'name');
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => { const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('product_category', true); formik.setFieldTouched('product_category', true);
@@ -167,17 +170,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
}; };
// Supplier (multi select) - using SWR to filter by category // Supplier (multi select) - using SWR to filter by category
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState(''); const {
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`; setInputValue: setSupplierSelectInputValue,
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR( options: supplierOptions,
suppliersUrl, isLoadingOptions: isLoadingSuppliers,
SupplierApi.getAllFetcher loadMore: loadMoreSuppliers,
); } = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
const supplierOptions = isResponseSuccess(suppliers) category: 'SAPRONAK',
? suppliers?.data });
.filter((sup) => sup.category === 'SAPRONAK')
.map((sup) => ({ value: sup.id, label: sup.name }))
: [];
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => { const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
const arr = Array.isArray(val) ? val : val ? [val] : []; const arr = Array.isArray(val) ? val : val ? [val] : [];
formik.setFieldTouched('supplier_ids', true); formik.setFieldTouched('supplier_ids', true);
@@ -291,6 +292,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={uomChangeHandler} onChange={uomChangeHandler}
options={uomOptions} options={uomOptions}
onInputChange={setUomSelectInputValue} onInputChange={setUomSelectInputValue}
onMenuScrollToBottom={loadMoreUoms}
isLoading={isLoadingUoms} isLoading={isLoadingUoms}
isError={ isError={
(formik.touched.uom || formik.touched.uom_id) && (formik.touched.uom || formik.touched.uom_id) &&
@@ -308,6 +310,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={categoryChangeHandler} onChange={categoryChangeHandler}
options={categoryOptions} options={categoryOptions}
onInputChange={setCategorySelectInputValue} onInputChange={setCategorySelectInputValue}
onMenuScrollToBottom={loadMoreCategories}
isLoading={isLoadingCategories} isLoading={isLoadingCategories}
isError={ isError={
(formik.touched.product_category || (formik.touched.product_category ||
@@ -412,6 +415,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
onChange={supplierChangeHandler} onChange={supplierChangeHandler}
options={supplierOptions} options={supplierOptions}
onInputChange={setSupplierSelectInputValue} onInputChange={setSupplierSelectInputValue}
onMenuScrollToBottom={loadMoreSuppliers}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
isError={ isError={
formik.touched.supplier_ids && formik.touched.supplier_ids &&
@@ -9,7 +9,10 @@ import useSWR from 'swr';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import Button from '@/components/Button'; import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput'; import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput'; import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal'; import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal'; import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission'; import RequirePermission from '@/components/helper/RequirePermission';
@@ -35,6 +38,8 @@ import { cn } from '@/lib/helper';
import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant'; import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant';
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 { Area } from '@/types/api/master-data/area';
import { Kandang } from '@/types/api/master-data/kandang';
interface WarehouseFormProps { interface WarehouseFormProps {
type?: 'add' | 'edit' | 'detail'; type?: 'add' | 'edit' | 'detail';
@@ -221,61 +226,28 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
const { setValues: formikSetValues } = formik; const { setValues: formikSetValues } = formik;
// Area // Area
const [areaSelectInputValue, setAreaSelectInputValue] = useState(''); const {
setInputValue: setAreaSelectInputValue,
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({ options: areaOptions,
search: areaSelectInputValue ?? '', isLoadingOptions: isLoadingAreaOptions,
}).toString()}`; loadMore: loadMoreAreas,
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
const { data: areas, isLoading: isLoadingAreas } = useSWR(
areasUrl,
AreaApi.getAllFetcher
);
const areaOptions = isResponseSuccess(areas)
? areas?.data.map((area) => ({
value: area.id,
label: area.name,
}))
: [];
// Location // Location
const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const {
setInputValue: setLocationSelectInputValue,
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({ options: locationOptions,
search: locationSelectInputValue ?? '', isLoadingOptions: isLoadingLocationOptions,
}).toString()}`; loadMore: loadMoreLocations,
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
const { data: locations, isLoading: isLoadingLocations } = useSWR(
locationsUrl,
LocationApi.getAllFetcher
);
const locationOptions = isResponseSuccess(locations)
? locations?.data.map((location) => ({
value: location.id,
label: location.name,
}))
: [];
// Kandang // Kandang
const [kandangSelectInputValue, setKandangSelectInputValue] = useState(''); const {
setInputValue: setKandangSelectInputValue,
const kandangsUrl = `${KandangApi.basePath}?${new URLSearchParams({ options: kandangOptions,
search: kandangSelectInputValue ?? '', isLoadingOptions: isLoadingKandangOptions,
}).toString()}`; loadMore: loadMoreKandangs,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
const { data: kandangs, isLoading: isLoadingKandangs } = useSWR(
kandangsUrl,
KandangApi.getAllFetcher
);
const kandangOptions = isResponseSuccess(kandangs)
? kandangs?.data.map((kandang) => ({
value: kandang.id,
label: kandang.name,
}))
: [];
const typeChangeHandler = (val: OptionType | OptionType[] | null) => { const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
formik.setFieldTouched('type', true); formik.setFieldTouched('type', true);
@@ -393,7 +365,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={areaChangeHandler} onChange={areaChangeHandler}
options={areaOptions} options={areaOptions}
onInputChange={setAreaSelectInputValue} onInputChange={setAreaSelectInputValue}
isLoading={isLoadingAreas} onMenuScrollToBottom={loadMoreAreas}
isLoading={isLoadingAreaOptions}
isError={formik.touched.areaId && Boolean(formik.errors.areaId)} isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
errorMessage={formik.errors.areaId as string} errorMessage={formik.errors.areaId as string}
isDisabled={type === 'detail'} isDisabled={type === 'detail'}
@@ -409,7 +382,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={locationChangeHandler} onChange={locationChangeHandler}
options={locationOptions} options={locationOptions}
onInputChange={setLocationSelectInputValue} onInputChange={setLocationSelectInputValue}
isLoading={isLoadingLocations} onMenuScrollToBottom={loadMoreLocations}
isLoading={isLoadingLocationOptions}
isError={ isError={
formik.touched.locationId && Boolean(formik.errors.locationId) formik.touched.locationId && Boolean(formik.errors.locationId)
} }
@@ -427,7 +401,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
onChange={kandangChangeHandler} onChange={kandangChangeHandler}
options={kandangOptions} options={kandangOptions}
onInputChange={setKandangSelectInputValue} onInputChange={setKandangSelectInputValue}
isLoading={isLoadingKandangs} onMenuScrollToBottom={loadMoreKandangs}
isLoading={isLoadingKandangOptions}
isError={ isError={
formik.touched.kandangId && Boolean(formik.errors.kandangId) formik.touched.kandangId && Boolean(formik.errors.kandangId)
} }
@@ -8,16 +8,16 @@ const FinanceTabs = () => {
const tabs = [ const tabs = [
{ {
id: '1', id: '1',
label: 'Kontrol Pembayaran Customer',
content: <CustomerPaymentTab />,
},
{
id: '2',
label: 'Rekapitulasi Hutang Ke Supplier', label: 'Rekapitulasi Hutang Ke Supplier',
content: <DebtSupplierTab />, content: <DebtSupplierTab />,
}, },
{
id: '2',
label: 'Kontrol Pembayaran Customer',
content: <CustomerPaymentTab />,
},
]; ];
return ( return (
@@ -176,7 +176,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<Text>No. PO</Text> <Text>No. PO</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl Terima</Text> <Text>Tgl Terima/Bayar</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl PO</Text> <Text>Tgl PO</Text>
@@ -191,19 +191,19 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<Text>Gudang</Text> <Text>Gudang</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Tgl Jatuh Tempo</Text> <Text>Jatuh Tempo</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Status Jatuh Tempo</Text> <Text>Status Jatuh Tempo</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Total Harga</Text> <Text>Nominal Pembelian (Rp)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Pembayaran</Text> <Text>Pembayaran (Rp)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}> <View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
<Text>Hutang</Text> <Text>Sisa Saldo Hutang (Rp)</Text>
</View> </View>
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}> <View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
<Text>Status</Text> <Text>Status</Text>
@@ -213,6 +213,65 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
</View> </View>
</View> </View>
{/* Initial Balance Row */}
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
<Text></Text>
</View>
<View
style={[
pdfStyles.tableCellRight,
{
flex: 1.5,
color: supplierReport.initial_balance < 0 ? 'red' : 'black',
},
]}
>
<Text>
{formatCurrency(supplierReport.initial_balance || 0)}
</Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text></Text>
</View>
</View>
{/* Table Body */} {/* Table Body */}
{supplierReport.rows.map((item, index) => ( {supplierReport.rows.map((item, index) => (
<View <View
@@ -297,10 +356,10 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
<View <View
style={[ style={[
pdfStyles.tableCellRight, pdfStyles.tableCellRight,
{ flex: 1.5, color: item.debt_price < 0 ? 'red' : 'black' }, { flex: 1.5, color: item.balance < 0 ? 'red' : 'black' },
]} ]}
> >
<Text>{formatCurrency(item.debt_price)}</Text> <Text>{formatCurrency(item.balance)}</Text>
</View> </View>
<View style={[pdfStyles.tableCell, { flex: 1 }]}> <View style={[pdfStyles.tableCell, { flex: 1 }]}>
<Text>{item.status || '-'}</Text> <Text>{item.status || '-'}</Text>
@@ -2,7 +2,7 @@
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { formatDate } from '@/lib/helper'; import { formatDate } from '@/lib/helper';
import { DebtSupplier } from '@/types/api/report/debt-supplier'; import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
interface DebtSupplierExportExcelParams { interface DebtSupplierExportExcelParams {
data: DebtSupplier[]; data: DebtSupplier[];
@@ -21,12 +21,29 @@ export const generateDebtSupplierExcel = (
const supplierData = supplierReport.rows; const supplierData = supplierReport.rows;
const supplierName = supplierReport.supplier.name || 'Unknown Supplier'; const supplierName = supplierReport.supplier.name || 'Unknown Supplier';
const excelData: { [key: string]: string | number }[] = supplierData.map( const excelData: { [key: string]: string | number }[] = [
(item, index) => ({ {
No: '',
'Nomor PR': '',
'Nomor PO': '',
'Tanggal Terima/Bayar': '',
'Tanggal PO': '',
'Aging (Hari)': '',
Area: '',
Gudang: '',
'Jatuh Tempo': '',
'Status Jatuh Tempo': '',
'Nominal Pembelian (Rp)': '',
'Pembayaran (Rp)': '',
'Sisa Saldo Hutang (Rp)': supplierReport.initial_balance || 0,
Status: '',
'Nomor Perjalanan': '',
},
...supplierData.map((item, index) => ({
No: index + 1, No: index + 1,
'Nomor PR': item.pr_number || '', 'Nomor PR': item.pr_number || '',
'Nomor PO': item.po_number || '', 'Nomor PO': item.po_number || '',
'Tanggal Terima': item.received_date 'Tanggal Terima/Bayar': item.received_date
? item.received_date != '-' ? item.received_date != '-'
? formatDate(item.received_date, 'MM/DD/YYYY') ? formatDate(item.received_date, 'MM/DD/YYYY')
: '-' : '-'
@@ -39,35 +56,35 @@ export const generateDebtSupplierExcel = (
'Aging (Hari)': item.aging || 0, 'Aging (Hari)': item.aging || 0,
Area: item.area?.name || '', Area: item.area?.name || '',
Gudang: item.warehouse?.name || '', Gudang: item.warehouse?.name || '',
'Tanggal Jatuh Tempo': item.due_date 'Jatuh Tempo': item.due_date
? item.due_date != '-' ? item.due_date != '-'
? formatDate(item.due_date, 'MM/DD/YYYY') ? formatDate(item.due_date, 'MM/DD/YYYY')
: '-' : '-'
: '-', : '-',
'Status Jatuh Tempo': item.due_status || '', 'Status Jatuh Tempo': item.due_status || '',
'Total Harga': item.total_price || 0, 'Nominal Pembelian (Rp)': item.total_price || 0,
'Harga Pembayaran': item.payment_price || 0, 'Pembayaran (Rp)': item.payment_price || 0,
'Harga Hutang': item.debt_price || 0, 'Sisa Saldo Hutang (Rp)': item.debt_price || 0,
Status: item.status || '', Status: item.status || '',
'Nomor Perjalanan': item.travel_number || '', 'Nomor Perjalanan': item.travel_number || '',
}) })),
); ];
if (supplierReport.total) { if (supplierReport.total) {
excelData.push({ excelData.push({
No: 'Total', No: 'Total',
'Nomor PR': '', 'Nomor PR': '',
'Nomor PO': '', 'Nomor PO': '',
'Tanggal Terima': '', 'Tanggal Terima/Bayar': '',
'Tanggal PO': '', 'Tanggal PO': '',
'Aging (Hari)': supplierReport.total.aging || 0, 'Aging (Hari)': supplierReport.total.aging || 0,
Area: '', Area: '',
Gudang: '', Gudang: '',
'Tanggal Jatuh Tempo': '', 'Jatuh Tempo': '',
'Status Jatuh Tempo': '', 'Status Jatuh Tempo': '',
'Total Harga': supplierReport.total.total_price || 0, 'Nominal Pembelian (Rp)': supplierReport.total.total_price || 0,
'Harga Pembayaran': supplierReport.total.payment_price || 0, 'Pembayaran (Rp)': supplierReport.total.payment_price || 0,
'Harga Hutang': supplierReport.total.debt_price || 0, 'Sisa Saldo Hutang (Rp)': supplierReport.total.debt_price || 0,
Status: '', Status: '',
'Nomor Perjalanan': '', 'Nomor Perjalanan': '',
}); });
@@ -79,16 +96,16 @@ export const generateDebtSupplierExcel = (
{ wch: 5 }, // No { wch: 5 }, // No
{ wch: 15 }, // Nomor PR { wch: 15 }, // Nomor PR
{ wch: 15 }, // Nomor PO { wch: 15 }, // Nomor PO
{ wch: 15 }, // Tanggal PR { wch: 15 }, // Tanggal Terima/Bayar
{ wch: 15 }, // Tanggal PO { wch: 15 }, // Tanggal PO
{ wch: 12 }, // Aging { wch: 12 }, // Aging
{ wch: 15 }, // Area { wch: 15 }, // Area
{ wch: 15 }, // Gudang { wch: 15 }, // Gudang
{ wch: 18 }, // Tanggal Jatuh Tempo { wch: 18 }, // Jatuh Tempo
{ wch: 18 }, // Status Jatuh Tempo { wch: 18 }, // Status Jatuh Tempo
{ wch: 15 }, // Total Harga { wch: 15 }, // Nominal Pembelian (Rp)
{ wch: 15 }, // Harga Pembayaran { wch: 15 }, // Pembayaran (Rp)
{ wch: 15 }, // Harga Hutang { wch: 15 }, // Sisa Saldo Hutang (Rp)
{ wch: 12 }, // Status { wch: 12 }, // Status
{ wch: 15 }, // Nomor Perjalanan { wch: 15 }, // Nomor Perjalanan
]; ];
@@ -0,0 +1,36 @@
import { OptionType } from '@/components/input/SelectInput';
import * as yup from 'yup';
export type DebtSupplierFilterType = {
startDate: string | null | undefined;
endDate: string | null | undefined;
supplierIds: OptionType[] | null | undefined;
filterBy: OptionType | null | undefined;
};
export const DebtSupplierFilterSchema: yup.ObjectSchema<DebtSupplierFilterType> =
yup.object({
startDate: yup.string().optional().notRequired(),
endDate: yup.string().optional().notRequired(),
supplierIds: yup
.array()
.of(
yup.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
)
.optional()
.notRequired(),
filterBy: yup
.object({
value: yup.mixed<string | number>().required(),
label: yup.string().required(),
})
.optional()
.notRequired(),
});
export type DebtSupplierFilterValues = yup.InferType<
typeof DebtSupplierFilterSchema
>;
@@ -13,7 +13,11 @@ import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
import { SupplierApi } from '@/services/api/master-data'; import { SupplierApi } from '@/services/api/master-data';
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier'; import {
DebtRow,
DebtSupplier,
DebtSupplierFilter,
} from '@/types/api/report/debt-supplier';
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX'; import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF'; import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
@@ -21,8 +25,14 @@ import { ColumnDef } from '@tanstack/react-table';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import useSWR from 'swr'; import useSWR from 'swr';
import Pagination from '@/components/Pagination';
import { DebtSupplierApi } from '@/services/api/report/debt-supplier'; import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
import { useFormik } from 'formik';
import {
DebtSupplierFilterSchema,
DebtSupplierFilterType,
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
import ButtonFilter from '@/components/helper/ButtonFilter';
const DebtSupplierTab = () => { const DebtSupplierTab = () => {
// ===== STATE MANAGEMENT ===== // ===== STATE MANAGEMENT =====
@@ -30,20 +40,15 @@ const DebtSupplierTab = () => {
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false); const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading; const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
// ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
start_date: undefined,
end_date: undefined,
supplier_ids: undefined,
filter_by: undefined,
});
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
// ===== FILTER STATE =====
const [filterSupplier, setFilterSupplier] = useState<OptionType[]>([]);
const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState('');
const [filterDateType, setFilterDateType] = useState<OptionType>();
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
const filterModal = useModal(); const filterModal = useModal();
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } = const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
@@ -59,48 +64,51 @@ const DebtSupplierTab = () => {
[] []
); );
// ===== FILTER HANDLERS ===== const handleFilterModalOpen = () => {
const handleResetFilters = useCallback(() => { filterModal.openModal();
setIsSubmitted(false); };
setFilterSupplier([]);
setFilterStartDate('');
setFilterEndDate('');
setFilterErrors({});
}, []);
const handleApplyFilters = useCallback(() => { // ===== FORMIK SETUP =====
const errors: Record<string, string> = {}; const formik = useFormik<DebtSupplierFilterType>({
initialValues: {
if (!filterStartDate) { startDate: null,
errors.start_date = 'Tanggal mulai wajib diisi'; endDate: null,
} supplierIds: null,
if (!filterEndDate) { filterBy: null,
errors.end_date = 'Tanggal akhir wajib diisi'; },
} validationSchema: DebtSupplierFilterSchema,
onSubmit: (values) => {
setFilterErrors(errors); setFilterParams({
start_date: values.startDate?.toString() || undefined,
if (Object.keys(errors).length === 0) { end_date: values.endDate?.toString() || undefined,
setIsSubmitted(true); supplier_ids:
setCurrentPage(1); values.supplierIds?.map((v) => String(v.value)).join(',') ||
undefined,
filter_by: values.filterBy?.value?.toString() || undefined,
});
filterModal.closeModal(); filterModal.closeModal();
} setIsSubmitted(true);
}, [filterModal, filterStartDate, filterEndDate]); },
onReset: (values) => {
setFilterParams({
start_date: undefined,
end_date: undefined,
supplier_ids: undefined,
filter_by: undefined,
});
setIsSubmitted(false);
},
});
// ===== DATA FETCHING ===== // ===== DATA FETCHING =====
const { data: debtSupplier, isLoading } = useSWR( const { data: debtSupplier, isLoading } = useSWR(
isSubmitted isSubmitted
? () => { ? () => {
const params = { const params = {
supplier_ids: supplier_ids: filterParams.supplier_ids,
filterSupplier.length > 0 filter_by: filterParams.filter_by,
? filterSupplier.map((v) => String(v.value)).join(',') start_date: filterParams.start_date,
: undefined, end_date: filterParams.end_date,
filter_by: filterDateType?.value,
start_date: filterStartDate || undefined,
end_date: filterEndDate || undefined,
page: currentPage,
limit: pageSize,
}; };
return ['debt-supplier-report', params]; return ['debt-supplier-report', params];
@@ -109,11 +117,9 @@ const DebtSupplierTab = () => {
([, params]) => ([, params]) =>
DebtSupplierApi.getDebtSupplierReport( DebtSupplierApi.getDebtSupplierReport(
params.supplier_ids, params.supplier_ids,
params.filter_by?.toString(), params.filter_by,
params.start_date, params.start_date,
params.end_date, params.end_date
params.page,
params.limit
) )
); );
@@ -135,13 +141,15 @@ const DebtSupplierTab = () => {
> => { > => {
const params = { const params = {
supplier_ids: supplier_ids:
filterSupplier.length > 0 formik.values.supplierIds && formik.values.supplierIds.length > 0
? filterSupplier.map((v) => String(v.value)).join(',') ? formik.values.supplierIds.map((v) => String(v.value)).join(',')
: undefined, : undefined,
filter_by: filterDateType?.value?.toString(), filter_by: formik.values.filterBy?.value?.toString() || undefined,
start_date: filterStartDate || undefined, start_date: formik.values.startDate || undefined,
end_date: filterEndDate || undefined, end_date: formik.values.endDate || undefined,
date_type: filterDateType ? filterDateType.value : undefined, date_type: formik.values.filterBy
? formik.values.filterBy.value
: undefined,
limit: 100, limit: 100,
page: 1, page: 1,
}; };
@@ -150,15 +158,18 @@ const DebtSupplierTab = () => {
params.supplier_ids, params.supplier_ids,
params.filter_by, params.filter_by,
params.start_date, params.start_date,
params.end_date, params.end_date
params.page,
params.limit
); );
return isResponseSuccess(response) return isResponseSuccess(response)
? (response.data as unknown as DebtSupplier[]) ? (response.data as unknown as DebtSupplier[])
: null; : null;
}, [filterSupplier, filterStartDate, filterEndDate]); }, [
formik.values.supplierIds,
formik.values.startDate,
formik.values.endDate,
formik.values.filterBy,
]);
// ===== EXPORT HANDLERS ===== // ===== EXPORT HANDLERS =====
const handleExportExcel = useCallback(async () => { const handleExportExcel = useCallback(async () => {
@@ -207,37 +218,18 @@ const DebtSupplierTab = () => {
} }
}, [debtSupplierExport]); }, [debtSupplierExport]);
// ===== PAGINATION HANDLERS =====
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const handleRowChange = (pageSize: number) => {
setPageSize(pageSize);
};
const handleNextPage = () => {
if (meta && currentPage < meta.total_pages) {
setCurrentPage(currentPage + 1);
}
};
const handlePrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [ const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
{ {
id: 'no', id: 'no',
header: 'No', header: 'No',
cell: (props) => props.row.index + 1, enableSorting: false,
cell: (props) => props.row.index,
}, },
{ {
id: 'pr_number', id: 'pr_number',
header: 'Nomor PR', header: 'Nomor PR',
accessorKey: 'pr_number', accessorKey: 'pr_number',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.pr_number; const value = props.row.original.pr_number;
return value || '-'; return value || '-';
@@ -247,6 +239,7 @@ const DebtSupplierTab = () => {
id: 'po_number', id: 'po_number',
header: 'Nomor PO', header: 'Nomor PO',
accessorKey: 'po_number', accessorKey: 'po_number',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.po_number; const value = props.row.original.po_number;
return value || '-'; return value || '-';
@@ -254,8 +247,9 @@ const DebtSupplierTab = () => {
}, },
{ {
id: 'received_date', id: 'received_date',
header: 'Tanggal Terima', header: 'Tanggal Terima/Bayar',
accessorKey: 'received_date', accessorKey: 'received_date',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.received_date; const value = props.row.original.received_date;
return value return value
@@ -269,6 +263,7 @@ const DebtSupplierTab = () => {
id: 'po_date', id: 'po_date',
header: 'Tanggal PO', header: 'Tanggal PO',
accessorKey: 'po_date', accessorKey: 'po_date',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.po_date; const value = props.row.original.po_date;
return value return value
@@ -282,6 +277,7 @@ const DebtSupplierTab = () => {
id: 'aging', id: 'aging',
header: 'Aging', header: 'Aging',
accessorKey: 'aging', accessorKey: 'aging',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.aging; const value = props.row.original.aging;
return <div className='text-center'>{formatNumber(value)} Hari</div>; return <div className='text-center'>{formatNumber(value)} Hari</div>;
@@ -295,6 +291,7 @@ const DebtSupplierTab = () => {
id: 'area', id: 'area',
header: 'Area', header: 'Area',
accessorKey: 'area', accessorKey: 'area',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.area?.name; const value = props.row.original.area?.name;
return value || '-'; return value || '-';
@@ -304,6 +301,7 @@ const DebtSupplierTab = () => {
id: 'warehouse', id: 'warehouse',
header: 'Gudang', header: 'Gudang',
accessorKey: 'warehouse', accessorKey: 'warehouse',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.warehouse?.name; const value = props.row.original.warehouse?.name;
return value || '-'; return value || '-';
@@ -311,8 +309,9 @@ const DebtSupplierTab = () => {
}, },
{ {
id: 'due_date', id: 'due_date',
header: 'Tanggal Jatuh Tempo', header: 'Jatuh Tempo',
accessorKey: 'due_date', accessorKey: 'due_date',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.due_date; const value = props.row.original.due_date;
return value return value
@@ -326,6 +325,7 @@ const DebtSupplierTab = () => {
id: 'due_status', id: 'due_status',
header: 'Status Jatuh Tempo', header: 'Status Jatuh Tempo',
accessorKey: 'due_status', accessorKey: 'due_status',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.due_status; const value = props.row.original.due_status;
return value || '-'; return value || '-';
@@ -333,8 +333,9 @@ const DebtSupplierTab = () => {
}, },
{ {
id: 'total_price', id: 'total_price',
header: 'Total Harga', header: 'Nominal Pembelian',
accessorKey: 'total_price', accessorKey: 'total_price',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.total_price; const value = props.row.original.total_price;
return ( return (
@@ -354,8 +355,9 @@ const DebtSupplierTab = () => {
}, },
{ {
id: 'payment_price', id: 'payment_price',
header: 'Harga Pembayaran', header: 'Pembayaran',
accessorKey: 'payment_price', accessorKey: 'payment_price',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.payment_price; const value = props.row.original.payment_price;
return ( return (
@@ -374,11 +376,12 @@ const DebtSupplierTab = () => {
}, },
}, },
{ {
id: 'debt_price', id: 'balance',
header: 'Harga Hutang', header: 'Sisa Saldo Hutang',
accessorKey: 'debt_price', accessorKey: 'balance',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.debt_price; const value = props.row.original.balance;
return ( return (
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}> <div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
{formatCurrency(value)} {formatCurrency(value)}
@@ -398,6 +401,7 @@ const DebtSupplierTab = () => {
id: 'status', id: 'status',
header: 'Status', header: 'Status',
accessorKey: 'status', accessorKey: 'status',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.status; const value = props.row.original.status;
return value || '-'; return value || '-';
@@ -407,6 +411,7 @@ const DebtSupplierTab = () => {
id: 'travel_number', id: 'travel_number',
header: 'Nomor Perjalanan', header: 'Nomor Perjalanan',
accessorKey: 'travel_number', accessorKey: 'travel_number',
enableSorting: false,
cell: (props) => { cell: (props) => {
const value = props.row.original.travel_number; const value = props.row.original.travel_number;
return value || '-'; return value || '-';
@@ -421,10 +426,11 @@ const DebtSupplierTab = () => {
className={{ wrapper: 'w-full', body: 'p-1!' }} className={{ wrapper: 'w-full', body: 'p-1!' }}
> >
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'> <div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
<Button variant='outline' onClick={filterModal.openModal}> <ButtonFilter
<Icon icon='heroicons:funnel' width={18} height={18} /> values={formik.values}
Filter onClick={handleFilterModalOpen}
</Button> variant='outline'
/>
<Dropdown <Dropdown
trigger={ trigger={
@@ -471,9 +477,14 @@ const DebtSupplierTab = () => {
collapsible={true} collapsible={true}
> >
<Table <Table
data={supplierReport.rows} data={[
{
balance: supplierReport.initial_balance,
} as DebtRow,
...supplierReport.rows,
]}
columns={getTableColumns(supplierReport)} columns={getTableColumns(supplierReport)}
pageSize={supplierReport.rows.length} pageSize={supplierReport.rows.length + 1}
renderFooter={supplierReport.rows.length > 0} renderFooter={supplierReport.rows.length > 0}
className={{ className={{
containerClassName: 'w-full', containerClassName: 'w-full',
@@ -493,26 +504,38 @@ const DebtSupplierTab = () => {
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap', 'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden', paginationClassName: 'hidden',
}} }}
renderCustomRow={(row) => {
if (row.index == 0) {
return (
<tr
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
key={row.index}
>
<td
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
colSpan={12}
></td>
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'>
<div
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
>
{formatCurrency(row.original.balance)}
</div>
</td>
<td
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
colSpan={2}
></td>
</tr>
);
}
}}
/> />
</Card> </Card>
); );
}) })
)} )}
</div> </div>
{meta && data.length > 0 && (
<div className='mt-6'>
<Pagination
currentPage={meta.page}
totalItems={meta.total_results}
onPageChange={handlePageChange}
onRowChange={handleRowChange}
onNextPage={handleNextPage}
onPrevPage={handlePrevPage}
rowOptions={[10, 25, 50, 100]}
itemsPerPage={meta.limit}
/>
</div>
)}
{/* Filter Modal */} {/* Filter Modal */}
<Modal <Modal
@@ -522,7 +545,11 @@ const DebtSupplierTab = () => {
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm', modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
}} }}
> >
<div className='space-y-6'> <form
className='space-y-6'
onSubmit={formik.handleSubmit}
onReset={formik.handleReset}
>
{/* Modal Header */} {/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'> <div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
<div className='flex items-center gap-2 text-primary'> <div className='flex items-center gap-2 text-primary'>
@@ -542,37 +569,31 @@ const DebtSupplierTab = () => {
<div> <div>
<DateInput <DateInput
label='Tanggal' label='Tanggal'
name='start_date' name='startDate'
value={filterStartDate} value={formik.values.startDate || ''}
onChange={(e) => { onChange={(e) => {
setFilterStartDate(e.target.value); formik.setFieldValue('startDate', e.target.value || null);
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
}} }}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={
formik.touched.startDate && !!formik.errors.startDate
}
errorMessage={formik.errors.startDate}
/> />
{filterErrors.start_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.start_date}
</p>
)}
</div> </div>
<div className='mt-auto'> <div className='mt-auto'>
<DateInput <DateInput
label=' ' label=' '
name='end_date' name='endDate'
value={filterEndDate} value={formik.values.endDate || ''}
onChange={(e) => { onChange={(e) => {
setFilterEndDate(e.target.value); formik.setFieldValue('endDate', e.target.value || null);
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
}} }}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={formik.touched.endDate && !!formik.errors.endDate}
errorMessage={formik.errors.endDate}
/> />
{filterErrors.end_date && (
<p className='text-red-500 text-sm mt-1'>
{filterErrors.end_date}
</p>
)}
</div> </div>
</div> </div>
@@ -582,15 +603,20 @@ const DebtSupplierTab = () => {
placeholder='Pilih Supplier' placeholder='Pilih Supplier'
isMulti isMulti
options={supplierOptions} options={supplierOptions}
value={filterSupplier} value={formik.values.supplierIds || []}
onChange={(val) => { onChange={(val) => {
setFilterSupplier( formik.setFieldValue(
Array.isArray(val) ? val : val ? [val] : [] 'supplierIds',
Array.isArray(val) ? val : val ? [val] : null
); );
}} }}
isLoading={isLoadingSuppliers} isLoading={isLoadingSuppliers}
isClearable isClearable
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isError={
formik.touched.supplierIds && !!formik.errors.supplierIds
}
errorMessage={formik.errors.supplierIds as string}
/> />
</div> </div>
@@ -599,12 +625,17 @@ const DebtSupplierTab = () => {
label='Filter Berdasarkan' label='Filter Berdasarkan'
placeholder='Pilih Filter Berdasarkan' placeholder='Pilih Filter Berdasarkan'
options={dataTypeOptions} options={dataTypeOptions}
value={filterDateType} value={formik.values.filterBy || null}
onChange={(val) => { onChange={(val) => {
setFilterDateType(val ? (val as OptionType) : undefined); formik.setFieldValue(
'filterBy',
val ? (val as OptionType) : null
);
}} }}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
isClearable isClearable
isError={formik.touched.filterBy && !!formik.errors.filterBy}
errorMessage={formik.errors.filterBy as string}
/> />
</div> </div>
</div> </div>
@@ -614,18 +645,15 @@ const DebtSupplierTab = () => {
<Button <Button
variant='soft' variant='soft'
className='ms-4 min-w-36 rounded-lg' className='ms-4 min-w-36 rounded-lg'
onClick={handleResetFilters} type='reset'
> >
Reset Filter Reset Filter
</Button> </Button>
<Button <Button className='me-4 min-w-36 rounded-lg' type='submit'>
className='me-4 min-w-36 rounded-lg'
onClick={handleApplyFilters}
>
Apply Filter Apply Filter
</Button> </Button>
</div> </div>
</div> </form>
</Modal> </Modal>
</> </>
); );
+64 -1
View File
@@ -1,4 +1,4 @@
import { FormikErrors } from 'formik'; import { FormikErrors, FormikValues } from 'formik';
export type ErrorMessage = { export type ErrorMessage = {
key: string; key: string;
@@ -69,3 +69,66 @@ export function getUniqueFormikErrors<T>(errors: FormikErrors<T>): string[] {
export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] { export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] {
return parseFormikErrors(errors); return parseFormikErrors(errors);
} }
/**
* Check if a value is considered "filled" (not empty)
* @param value - Value to check
* @returns True if value is filled, false otherwise
*/
function isValueFilled(value: unknown): boolean {
// Check for null or undefined
if (value === null || value === undefined) {
return false;
}
// Check for empty string
if (typeof value === 'string' && value.trim() === '') {
return false;
}
// Check for empty array
if (Array.isArray(value) && value.length === 0) {
return false;
}
// Check for empty object (but not Date or other special objects)
if (
typeof value === 'object' &&
!Array.isArray(value) &&
!(value instanceof Date) &&
Object.keys(value).length === 0
) {
return false;
}
return true;
}
/**
* Count the number of filled (non-empty) values in Formik values object
* @param values - Formik values object
* @returns Number of filled values
* @example
* const values = {
* name: 'John',
* email: '',
* age: null,
* tags: ['tag1', 'tag2'],
* emptyArray: [],
* };
* getFilledFormikValuesCount(values); // Returns 2 (name and tags)
*/
export function getFilledFormikValuesCount<T extends FormikValues>(
values: T
): number {
let count = 0;
Object.keys(values).forEach((key) => {
const value = values[key];
if (isValueFilled(value)) {
count++;
}
});
return count;
}
+1 -5
View File
@@ -15,9 +15,7 @@ export class DebtSupplierApiService extends BaseApiService<
supplier_ids?: string, supplier_ids?: string,
filter_by?: string, filter_by?: string,
start_date?: string, start_date?: string,
end_date?: string, end_date?: string
page?: number,
limit?: number
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> { ): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>( return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
`debt-supplier`, `debt-supplier`,
@@ -28,8 +26,6 @@ export class DebtSupplierApiService extends BaseApiService<
filter_by: filter_by, filter_by: filter_by,
start_date: start_date, start_date: start_date,
end_date: end_date, end_date: end_date,
page: page,
limit: limit,
}, },
} }
); );
+8
View File
@@ -33,3 +33,11 @@ export interface DebtRow {
travel_number: string; travel_number: string;
balance: number; balance: number;
} }
// Filter Param
export interface DebtSupplierFilter {
start_date?: string;
end_date?: string;
supplier_ids?: string;
filter_by?: string;
}