mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu
This commit is contained in:
@@ -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;
|
||||||
@@ -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 {
|
|
||||||
|
return pages.flatMap((page) =>
|
||||||
|
isResponseSuccess(page)
|
||||||
|
? page.data.map((item) => ({
|
||||||
value: getByPath<T, number>(item, valueKey as string),
|
value: getByPath<T, number>(item, valueKey as string),
|
||||||
label: getByPath<T, string>(item, labelKey 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,
|
||||||
|
filter_by: formik.values.filterBy?.value?.toString() || undefined,
|
||||||
|
start_date: formik.values.startDate || undefined,
|
||||||
|
end_date: formik.values.endDate || undefined,
|
||||||
|
date_type: formik.values.filterBy
|
||||||
|
? formik.values.filterBy.value
|
||||||
: undefined,
|
: undefined,
|
||||||
filter_by: filterDateType?.value?.toString(),
|
|
||||||
start_date: filterStartDate || undefined,
|
|
||||||
end_date: filterEndDate || undefined,
|
|
||||||
date_type: filterDateType ? filterDateType.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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user