mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into fix/dashboard
This commit is contained in:
@@ -148,7 +148,11 @@ const Card = ({
|
||||
const hasContent = children || actions || footer;
|
||||
|
||||
const titleContent = (
|
||||
<div className='group flex items-center !justify-between w-full'>
|
||||
<div
|
||||
className={
|
||||
`group flex items-center justify-between! w-full` + getTitleClasses()
|
||||
}
|
||||
>
|
||||
<div className='flex-1'>
|
||||
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||
@@ -156,7 +160,7 @@ const Card = ({
|
||||
{collapsible && (
|
||||
<button
|
||||
onClick={() => handleCollapsedChange(!isCollapsed)}
|
||||
className='btn btn-ghost btn-sm btn-circle'
|
||||
className={`btn btn-ghost btn-sm btn-circle` + getTitleClasses()}
|
||||
aria-label={isCollapsed ? 'Expand content' : 'Collapse content'}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -9,15 +9,20 @@ import Select, {
|
||||
SingleValue,
|
||||
components as ReactSelectComponents,
|
||||
ControlProps,
|
||||
MenuListProps,
|
||||
} from 'react-select';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import makeAnimated from 'react-select/animated';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { cn, getByPath } from '@/lib/helper';
|
||||
import useSWR from 'swr';
|
||||
import useSWRInfinite from 'swr/infinite';
|
||||
import { httpClientFetcher } from '@/services/http/client';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import {
|
||||
BaseApiResponse,
|
||||
ErrorApiResponse,
|
||||
SuccessApiResponse,
|
||||
} from '@/types/api/api-general';
|
||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
export interface OptionType {
|
||||
value: string | number;
|
||||
@@ -35,6 +40,7 @@ interface SelectInputBaseProps<T = OptionType> {
|
||||
bottomLabel?: ReactNode;
|
||||
options: T[];
|
||||
optionComponent?: OptionComponent<T>;
|
||||
components?: Partial<typeof ReactSelectComponents>;
|
||||
isDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
isClearable?: boolean;
|
||||
@@ -56,9 +62,13 @@ interface SelectInputBaseProps<T = OptionType> {
|
||||
onInputChange?: (search: string) => void;
|
||||
startAdornment?: ReactNode;
|
||||
menuPortalTarget?: HTMLElement | null;
|
||||
closeMenuOnSelect?: boolean;
|
||||
hideSelectedOptions?: boolean;
|
||||
onMenuScrollToBottom?: ((event: WheelEvent | TouchEvent) => void) | undefined;
|
||||
}
|
||||
|
||||
interface SelectInputProps<T = OptionType> extends SelectInputBaseProps<T> {
|
||||
export interface SelectInputProps<T = OptionType>
|
||||
extends SelectInputBaseProps<T> {
|
||||
createables?: boolean;
|
||||
value?: T | T[] | null;
|
||||
onChange?: (val: T | T[] | null) => void;
|
||||
@@ -93,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 {
|
||||
label,
|
||||
@@ -101,6 +134,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
onChange,
|
||||
options,
|
||||
optionComponent,
|
||||
components: customComponents,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
isClearable,
|
||||
@@ -119,6 +153,9 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
onInputChange,
|
||||
startAdornment,
|
||||
menuPortalTarget,
|
||||
closeMenuOnSelect,
|
||||
hideSelectedOptions,
|
||||
onMenuScrollToBottom,
|
||||
} = props;
|
||||
|
||||
const [internalInputValue, setInternalInputValue] = useState('');
|
||||
@@ -128,14 +165,18 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
|
||||
const components = useMemo(() => {
|
||||
const base = isAnimated ? animatedComponents : {};
|
||||
const customComponents = { ...base, IndicatorSeparator: () => null };
|
||||
const mergedComponents = { ...base, IndicatorSeparator: () => null };
|
||||
|
||||
if (startAdornment) {
|
||||
customComponents.Control = CustomControl;
|
||||
mergedComponents.Control = CustomControl;
|
||||
}
|
||||
|
||||
return customComponents;
|
||||
}, [isAnimated, startAdornment]);
|
||||
if (customComponents) {
|
||||
Object.assign(mergedComponents, customComponents);
|
||||
}
|
||||
|
||||
return mergedComponents;
|
||||
}, [isAnimated, startAdornment, customComponents]);
|
||||
|
||||
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
|
||||
if (meta.action === 'input-change') setInternalInputValue(val);
|
||||
@@ -205,6 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
isRtl={isRtl}
|
||||
isSearchable={isSearchable}
|
||||
placeholder={placeholder}
|
||||
closeMenuOnSelect={closeMenuOnSelect}
|
||||
hideSelectedOptions={hideSelectedOptions}
|
||||
className={cn('w-full', className?.select)}
|
||||
classNames={{
|
||||
...(!startAdornment && {
|
||||
@@ -256,6 +299,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
components={{
|
||||
...components,
|
||||
...(optionComponent ? { Option: optionComponent } : {}),
|
||||
MenuList: CustomMenuList,
|
||||
}}
|
||||
{...(startAdornment && {
|
||||
shouldShowAdornment,
|
||||
@@ -269,6 +313,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
||||
styles={{
|
||||
menuPortal: (base) => ({ ...base, zIndex: 9999 }),
|
||||
}}
|
||||
onMenuScrollToBottom={onMenuScrollToBottom}
|
||||
/>
|
||||
|
||||
{isError && <p className='w-full text-sm text-error'>{errorMessage}</p>}
|
||||
@@ -288,34 +333,96 @@ const useSelect = <T,>(
|
||||
) => {
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const optionsUrlParams = useMemo(() => {
|
||||
return new URLSearchParams({
|
||||
const pageKey = 'page';
|
||||
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 ?? '',
|
||||
...params,
|
||||
[pageKey]: String(pageIndex + 1),
|
||||
[limitKey]: String(limit),
|
||||
}).toString();
|
||||
}, [inputValue, searchKey, params]);
|
||||
|
||||
const optionsUrl = `${basePath}?${optionsUrlParams}`;
|
||||
return `${basePath}?${qs}`;
|
||||
};
|
||||
|
||||
const { data, isLoading } = useSWR(optionsUrl, async (url) => {
|
||||
return await httpClientFetcher<BaseApiResponse<T[]>>(url);
|
||||
});
|
||||
const {
|
||||
data: pages,
|
||||
isLoading,
|
||||
isValidating,
|
||||
size,
|
||||
setSize,
|
||||
} = useSWRInfinite<BaseApiResponse<T[]>>(getKey, (url) =>
|
||||
httpClientFetcher<BaseApiResponse<T[]>>(url)
|
||||
);
|
||||
|
||||
const options = isResponseSuccess(data)
|
||||
? data.data.map((item) => {
|
||||
return {
|
||||
value: getByPath<T, number>(item, valueKey as string),
|
||||
label: getByPath<T, string>(item, labelKey as string),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
const options = useMemo(() => {
|
||||
if (!pages) return [];
|
||||
|
||||
return pages.flatMap((page) =>
|
||||
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 {
|
||||
inputValue,
|
||||
setInputValue,
|
||||
|
||||
options,
|
||||
isLoadingOptions: isLoading,
|
||||
rawData: data,
|
||||
rawData: isResponseSuccess(pages?.[latestPagesIndex])
|
||||
? formattedSuccessRawData
|
||||
: formattedErrorRawData,
|
||||
|
||||
isLoadingOptions: isLoading || isValidating,
|
||||
isLoadingMore: isValidating && size > 1,
|
||||
hasMore,
|
||||
loadMore,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
OptionProps,
|
||||
GroupBase,
|
||||
components as ReactSelectComponents,
|
||||
} from 'react-select';
|
||||
import SelectInput, { OptionType, SelectInputProps } from './SelectInput';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
interface SelectInputCheckboxProps<T = OptionType>
|
||||
extends Omit<
|
||||
SelectInputProps<T>,
|
||||
'closeMenuOnSelect' | 'hideSelectedOptions' | 'optionComponent'
|
||||
> {
|
||||
closeMenuOnSelect?: boolean;
|
||||
hideSelectedOptions?: boolean;
|
||||
}
|
||||
|
||||
const CheckboxOption = <
|
||||
T extends OptionType,
|
||||
IsMulti extends boolean,
|
||||
Group extends GroupBase<T>,
|
||||
>(
|
||||
props: OptionProps<T, IsMulti, Group>
|
||||
) => {
|
||||
const { isSelected, label, innerRef, innerProps, className } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={innerRef}
|
||||
{...innerProps}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 cursor-pointer',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={isSelected}
|
||||
onChange={() => null}
|
||||
className='checkbox checkbox-sm checkbox-primary pointer-events-none'
|
||||
/>
|
||||
<label className='cursor-pointer flex-1 select-none'>{label}</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectInputCheckbox = <T extends OptionType>(
|
||||
props: SelectInputCheckboxProps<T>
|
||||
) => {
|
||||
const {
|
||||
closeMenuOnSelect = false,
|
||||
hideSelectedOptions = false,
|
||||
isMulti = true,
|
||||
className,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const customComponents = useMemo(() => {
|
||||
return {
|
||||
Option: CheckboxOption as typeof ReactSelectComponents.Option,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SelectInput<T>
|
||||
{...restProps}
|
||||
isMulti={isMulti}
|
||||
closeMenuOnSelect={closeMenuOnSelect}
|
||||
hideSelectedOptions={hideSelectedOptions}
|
||||
className={{
|
||||
...className,
|
||||
select: cn(className?.select, 'select-checkbox'),
|
||||
}}
|
||||
components={customComponents}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectInputCheckbox;
|
||||
@@ -23,13 +23,17 @@ import TextInput from '@/components/input/TextInput';
|
||||
import { cn } from '@/lib/helper';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
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 { UserApi } from '@/services/api/user';
|
||||
import { TYPE_OPTIONS } from '@/config/constant';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { User } from '@/types/api/api-general';
|
||||
|
||||
interface CustomerFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -47,25 +51,15 @@ const CustomerForm = ({
|
||||
// Setup State
|
||||
const [customerFormErrorMessage, setCustomerFormErrorMessage] = useState('');
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [picSelectInputValue, setPicSelectInputValue] = useState('');
|
||||
|
||||
// Fetch Data
|
||||
const picUrl = `${UserApi.basePath}?${new URLSearchParams({
|
||||
search: picSelectInputValue ?? '',
|
||||
})}`;
|
||||
|
||||
const { data: pic, isLoading: isLoadingPic } = useSWR(
|
||||
picUrl,
|
||||
UserApi.getAllFetcher
|
||||
);
|
||||
const {
|
||||
setInputValue: setPicSelectInputValue,
|
||||
options: picOptions,
|
||||
isLoadingOptions: isLoadingPicOptions,
|
||||
loadMore: loadMorePic,
|
||||
} = useSelect<User>(UserApi.basePath, 'id', 'name');
|
||||
|
||||
// -- Options data mapping
|
||||
const picOptions = isResponseSuccess(pic)
|
||||
? pic?.data.map((area) => ({
|
||||
value: area.id,
|
||||
label: area.name,
|
||||
}))
|
||||
: [];
|
||||
const typeOptions = TYPE_OPTIONS;
|
||||
|
||||
// Handler Event
|
||||
@@ -240,11 +234,12 @@ const CustomerForm = ({
|
||||
required
|
||||
placeholder='Pilih PIC'
|
||||
label='PIC'
|
||||
value={formik.values.pic ?? undefined}
|
||||
value={formik.values.pic?.value ? formik.values.pic : undefined}
|
||||
onChange={picChangeHandler}
|
||||
options={picOptions}
|
||||
onInputChange={setPicSelectInputValue}
|
||||
isLoading={isLoadingPic}
|
||||
onMenuScrollToBottom={loadMorePic}
|
||||
isLoading={isLoadingPicOptions}
|
||||
isError={formik.touched.picId && Boolean(formik.errors.picId)}
|
||||
errorMessage={formik.errors.picId as string}
|
||||
isDisabled={formType === 'detail'}
|
||||
|
||||
@@ -9,7 +9,10 @@ import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
@@ -31,6 +34,7 @@ import { UserApi } from '@/services/api/user';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { User } from '@/types/api/api-general';
|
||||
|
||||
interface KandangFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -128,23 +132,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
// location
|
||||
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
|
||||
|
||||
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
||||
search: locationSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
||||
locationsUrl,
|
||||
LocationApi.getAllFetcher
|
||||
);
|
||||
|
||||
const locationOptions = isResponseSuccess(locations)
|
||||
? locations?.data.map((location) => ({
|
||||
value: location.id,
|
||||
label: location.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setLocationSelectInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('location', true);
|
||||
@@ -155,23 +148,12 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
};
|
||||
|
||||
// PIC
|
||||
const [picSelectInputValue, setPicSelectInputValue] = useState('');
|
||||
|
||||
const picsUrl = `${UserApi.basePath}?${new URLSearchParams({
|
||||
search: picSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: pics, isLoading: isLoadingPics } = useSWR(
|
||||
picsUrl,
|
||||
LocationApi.getAllFetcher
|
||||
);
|
||||
|
||||
const picOptions = isResponseSuccess(pics)
|
||||
? pics?.data.map((pic) => ({
|
||||
value: pic.id,
|
||||
label: pic.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setPicSelectInputValue,
|
||||
options: picOptions,
|
||||
isLoadingOptions: isLoadingPicOptions,
|
||||
loadMore: loadMorePics,
|
||||
} = useSelect<User>(UserApi.basePath, 'id', 'name');
|
||||
|
||||
const picChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('pic', true);
|
||||
@@ -249,7 +231,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
onChange={locationChangeHandler}
|
||||
options={locationOptions}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
isLoading={isLoadingLocations}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isError={
|
||||
formik.touched.locationId && Boolean(formik.errors.locationId)
|
||||
}
|
||||
@@ -280,7 +263,8 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
||||
onChange={picChangeHandler}
|
||||
options={picOptions}
|
||||
onInputChange={setPicSelectInputValue}
|
||||
isLoading={isLoadingPics}
|
||||
onMenuScrollToBottom={loadMorePics}
|
||||
isLoading={isLoadingPicOptions}
|
||||
isError={formik.touched.picId && Boolean(formik.errors.picId)}
|
||||
errorMessage={formik.errors.picId as string}
|
||||
isDisabled={type === 'detail'}
|
||||
|
||||
@@ -9,7 +9,10 @@ import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
@@ -29,6 +32,7 @@ import { AreaApi, LocationApi } from '@/services/api/master-data';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
|
||||
interface LocationFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -117,23 +121,12 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
||||
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
|
||||
|
||||
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
||||
search: areaSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: areas, isLoading: isLoadingAreas } = useSWR(
|
||||
areasUrl,
|
||||
AreaApi.getAllFetcher
|
||||
);
|
||||
|
||||
const areaOptions = isResponseSuccess(areas)
|
||||
? areas?.data.map((area) => ({
|
||||
value: area.id,
|
||||
label: area.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setAreaSelectInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('area', true);
|
||||
@@ -224,7 +217,8 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
|
||||
onChange={areaChangeHandler}
|
||||
options={areaOptions}
|
||||
onInputChange={setAreaSelectInputValue}
|
||||
isLoading={isLoadingAreas}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
|
||||
errorMessage={formik.errors.areaId as string}
|
||||
isDisabled={type === 'detail'}
|
||||
|
||||
@@ -9,7 +9,10 @@ import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
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 { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
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 {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -129,23 +134,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
// UOM
|
||||
const [uomSelectInputValue, setUomSelectInputValue] = useState('');
|
||||
|
||||
const uomsUrl = `${UomApi.basePath}?${new URLSearchParams({
|
||||
search: uomSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: uoms, isLoading: isLoadingUoms } = useSWR(
|
||||
uomsUrl,
|
||||
UomApi.getAllFetcher
|
||||
);
|
||||
|
||||
const uomOptions = isResponseSuccess(uoms)
|
||||
? uoms?.data.map((uom) => ({
|
||||
value: uom.id,
|
||||
label: uom.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setUomSelectInputValue,
|
||||
options: uomOptions,
|
||||
isLoadingOptions: isLoadingUomOptions,
|
||||
loadMore: loadMoreUoms,
|
||||
} = useSelect<Uom>(UomApi.basePath, 'id', 'name');
|
||||
|
||||
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('uom', true);
|
||||
@@ -156,25 +150,12 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
};
|
||||
|
||||
// supplier
|
||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||
|
||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({
|
||||
search: supplierSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
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 {
|
||||
setInputValue: setSupplierSelectInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSupplierOptions,
|
||||
loadMore: loadMoreSuppliers,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||
|
||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('suppliers', true);
|
||||
@@ -264,7 +245,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
onChange={uomChangeHandler}
|
||||
options={uomOptions}
|
||||
onInputChange={setUomSelectInputValue}
|
||||
isLoading={isLoadingUoms}
|
||||
isLoading={isLoadingUomOptions}
|
||||
onMenuScrollToBottom={loadMoreUoms}
|
||||
isError={formik.touched.uomId && Boolean(formik.errors.uomId)}
|
||||
errorMessage={formik.errors.uomId as string}
|
||||
isDisabled={type === 'detail'}
|
||||
@@ -278,7 +260,8 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
||||
onChange={supplierChangeHandler}
|
||||
options={supplierOptions ?? []}
|
||||
onInputChange={setSupplierSelectInputValue}
|
||||
isLoading={isLoadingSuppliers}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isLoading={isLoadingSupplierOptions}
|
||||
isError={
|
||||
formik.touched.suppliers && Boolean(formik.errors.suppliers)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
import { cn } from '@/lib/helper';
|
||||
import { PRODUCT_FLAG_OPTIONS } from '@/config/constant';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
|
||||
interface ProductFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -145,6 +146,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
setInputValue: setUomSelectInputValue,
|
||||
options: uomOptions,
|
||||
isLoadingOptions: isLoadingUoms,
|
||||
loadMore: loadMoreUoms,
|
||||
} = useSelect(UomApi.basePath, 'id', 'name');
|
||||
const uomChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('uom', true);
|
||||
@@ -158,6 +160,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
setInputValue: setCategorySelectInputValue,
|
||||
options: categoryOptions,
|
||||
isLoadingOptions: isLoadingCategories,
|
||||
loadMore: loadMoreCategories,
|
||||
} = useSelect(ProductCategoryApi.basePath, 'id', 'name');
|
||||
const categoryChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('product_category', true);
|
||||
@@ -167,17 +170,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
};
|
||||
|
||||
// Supplier (multi select) - using SWR to filter by category
|
||||
const [supplierSelectInputValue, setSupplierSelectInputValue] = useState('');
|
||||
const suppliersUrl = `${SupplierApi.basePath}?${new URLSearchParams({ search: supplierSelectInputValue ?? '' }).toString()}`;
|
||||
const { data: suppliers, isLoading: isLoadingSuppliers } = useSWR(
|
||||
suppliersUrl,
|
||||
SupplierApi.getAllFetcher
|
||||
);
|
||||
const supplierOptions = isResponseSuccess(suppliers)
|
||||
? suppliers?.data
|
||||
.filter((sup) => sup.category === 'SAPRONAK')
|
||||
.map((sup) => ({ value: sup.id, label: sup.name }))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setSupplierSelectInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSuppliers,
|
||||
loadMore: loadMoreSuppliers,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'SAPRONAK',
|
||||
});
|
||||
|
||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
||||
formik.setFieldTouched('supplier_ids', true);
|
||||
@@ -291,6 +292,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
onChange={uomChangeHandler}
|
||||
options={uomOptions}
|
||||
onInputChange={setUomSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreUoms}
|
||||
isLoading={isLoadingUoms}
|
||||
isError={
|
||||
(formik.touched.uom || formik.touched.uom_id) &&
|
||||
@@ -308,6 +310,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
onChange={categoryChangeHandler}
|
||||
options={categoryOptions}
|
||||
onInputChange={setCategorySelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreCategories}
|
||||
isLoading={isLoadingCategories}
|
||||
isError={
|
||||
(formik.touched.product_category ||
|
||||
@@ -412,6 +415,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
||||
onChange={supplierChangeHandler}
|
||||
options={supplierOptions}
|
||||
onInputChange={setSupplierSelectInputValue}
|
||||
onMenuScrollToBottom={loadMoreSuppliers}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isError={
|
||||
formik.touched.supplier_ids &&
|
||||
|
||||
@@ -9,7 +9,10 @@ import useSWR from 'swr';
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
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 ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import RequirePermission from '@/components/helper/RequirePermission';
|
||||
@@ -35,6 +38,8 @@ import { cn } from '@/lib/helper';
|
||||
import { WAREHOUSE_TYPE_OPTIONS } from '@/config/constant';
|
||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||
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 {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
@@ -221,61 +226,28 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
const { setValues: formikSetValues } = formik;
|
||||
|
||||
// Area
|
||||
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
|
||||
|
||||
const areasUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
||||
search: areaSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: areas, isLoading: isLoadingAreas } = useSWR(
|
||||
areasUrl,
|
||||
AreaApi.getAllFetcher
|
||||
);
|
||||
|
||||
const areaOptions = isResponseSuccess(areas)
|
||||
? areas?.data.map((area) => ({
|
||||
value: area.id,
|
||||
label: area.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setAreaSelectInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreaOptions,
|
||||
loadMore: loadMoreAreas,
|
||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||
|
||||
// Location
|
||||
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
|
||||
|
||||
const locationsUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
||||
search: locationSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
||||
locationsUrl,
|
||||
LocationApi.getAllFetcher
|
||||
);
|
||||
|
||||
const locationOptions = isResponseSuccess(locations)
|
||||
? locations?.data.map((location) => ({
|
||||
value: location.id,
|
||||
label: location.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setLocationSelectInputValue,
|
||||
options: locationOptions,
|
||||
isLoadingOptions: isLoadingLocationOptions,
|
||||
loadMore: loadMoreLocations,
|
||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||
|
||||
// Kandang
|
||||
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
|
||||
|
||||
const kandangsUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
||||
search: kandangSelectInputValue ?? '',
|
||||
}).toString()}`;
|
||||
|
||||
const { data: kandangs, isLoading: isLoadingKandangs } = useSWR(
|
||||
kandangsUrl,
|
||||
KandangApi.getAllFetcher
|
||||
);
|
||||
|
||||
const kandangOptions = isResponseSuccess(kandangs)
|
||||
? kandangs?.data.map((kandang) => ({
|
||||
value: kandang.id,
|
||||
label: kandang.name,
|
||||
}))
|
||||
: [];
|
||||
const {
|
||||
setInputValue: setKandangSelectInputValue,
|
||||
options: kandangOptions,
|
||||
isLoadingOptions: isLoadingKandangOptions,
|
||||
loadMore: loadMoreKandangs,
|
||||
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||
|
||||
const typeChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldTouched('type', true);
|
||||
@@ -393,7 +365,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
onChange={areaChangeHandler}
|
||||
options={areaOptions}
|
||||
onInputChange={setAreaSelectInputValue}
|
||||
isLoading={isLoadingAreas}
|
||||
onMenuScrollToBottom={loadMoreAreas}
|
||||
isLoading={isLoadingAreaOptions}
|
||||
isError={formik.touched.areaId && Boolean(formik.errors.areaId)}
|
||||
errorMessage={formik.errors.areaId as string}
|
||||
isDisabled={type === 'detail'}
|
||||
@@ -409,7 +382,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
onChange={locationChangeHandler}
|
||||
options={locationOptions}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
isLoading={isLoadingLocations}
|
||||
onMenuScrollToBottom={loadMoreLocations}
|
||||
isLoading={isLoadingLocationOptions}
|
||||
isError={
|
||||
formik.touched.locationId && Boolean(formik.errors.locationId)
|
||||
}
|
||||
@@ -427,7 +401,8 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
||||
onChange={kandangChangeHandler}
|
||||
options={kandangOptions}
|
||||
onInputChange={setKandangSelectInputValue}
|
||||
isLoading={isLoadingKandangs}
|
||||
onMenuScrollToBottom={loadMoreKandangs}
|
||||
isLoading={isLoadingKandangOptions}
|
||||
isError={
|
||||
formik.touched.kandangId && Boolean(formik.errors.kandangId)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import SelectInput, {
|
||||
useSelect,
|
||||
OptionType,
|
||||
} from '@/components/input/SelectInput';
|
||||
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { CustomerApi } from '@/services/api/master-data';
|
||||
import { FinanceApi } from '@/services/api/report/finance-report';
|
||||
import { UserApi } from '@/services/api/user';
|
||||
import Table from '@/components/Table';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||
@@ -18,7 +20,6 @@ import {
|
||||
CustomerPaymentSummary,
|
||||
} from '@/types/api/report/customer-payment';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import Pagination from '@/components/Pagination';
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown from '@/components/Dropdown';
|
||||
import MenuItem from '@/components/menu/MenuItem';
|
||||
@@ -37,31 +38,34 @@ const CustomerPaymentTab = () => {
|
||||
|
||||
// ===== PAGINATION STATE =====
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [pageSize] = useState(10);
|
||||
|
||||
// ===== SUBMISSION STATE =====
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
// ===== FILTER STATE =====
|
||||
const [filterCustomer, setFilterCustomer] = useState<OptionType[]>([]);
|
||||
const [filterSales, setFilterSales] = useState<OptionType[]>([]);
|
||||
const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
|
||||
[]
|
||||
);
|
||||
const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
|
||||
const [filterStartDate, setFilterStartDate] = useState('');
|
||||
const [filterEndDate, setFilterEndDate] = useState('');
|
||||
|
||||
const filterModal = useModal();
|
||||
|
||||
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
|
||||
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomers,
|
||||
loadMore: loadMoreCustomers,
|
||||
hasMore: hasMoreCustomers,
|
||||
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const salesOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'Sales A', label: 'Sales A' },
|
||||
{ value: 'Sales B', label: 'Sales B' },
|
||||
{ value: 'Sales C', label: 'Sales C' },
|
||||
// TODO: Fetch sales options from API
|
||||
],
|
||||
[]
|
||||
);
|
||||
const {
|
||||
options: salesOptions,
|
||||
isLoadingOptions: isLoadingSales,
|
||||
loadMore: loadMoreSales,
|
||||
hasMore: hasMoreSales,
|
||||
} = useSelect(UserApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const dataTypeOptions = useMemo(
|
||||
() => [{ value: 'do_date', label: 'Tanggal Jual' }],
|
||||
@@ -115,6 +119,41 @@ const CustomerPaymentTab = () => {
|
||||
filterModal.closeModal();
|
||||
}, [filterModal]);
|
||||
|
||||
// ===== ACTIVE FILTERS COUNT =====
|
||||
const activeFiltersCount = useMemo(() => {
|
||||
let count = 0;
|
||||
|
||||
// Date filter (start_date + end_date = 1 filter)
|
||||
if (filterStartDate || filterEndDate) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Customer filter
|
||||
if (filterCustomer.length > 0) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Sales filter
|
||||
if (filterSales.length > 0) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
// Filter by (always count if submitted)
|
||||
if (isSubmitted) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}, [
|
||||
filterStartDate,
|
||||
filterEndDate,
|
||||
filterCustomer,
|
||||
filterSales,
|
||||
isSubmitted,
|
||||
]);
|
||||
|
||||
const hasFilters = activeFiltersCount > 0;
|
||||
|
||||
// ===== DATA FETCHING =====
|
||||
const { data: customerPayment, isLoading } = useSWR(
|
||||
isSubmitted
|
||||
@@ -124,7 +163,7 @@ const CustomerPaymentTab = () => {
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
sales:
|
||||
sales_id:
|
||||
filterSales.length > 0
|
||||
? filterSales.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
@@ -141,7 +180,7 @@ const CustomerPaymentTab = () => {
|
||||
([, params]) =>
|
||||
FinanceApi.getCustomerPaymentReport(
|
||||
params.customer_id,
|
||||
params.sales,
|
||||
params.sales_id,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
@@ -158,11 +197,6 @@ const CustomerPaymentTab = () => {
|
||||
[customerPayment]
|
||||
);
|
||||
|
||||
const meta =
|
||||
isResponseSuccess(customerPayment) && customerPayment?.meta
|
||||
? customerPayment.meta
|
||||
: null;
|
||||
|
||||
// ===== EXPORT DATA FETCHER =====
|
||||
const customerPaymentExport = useCallback(async (): Promise<
|
||||
CustomerPaymentReport[] | null
|
||||
@@ -172,7 +206,7 @@ const CustomerPaymentTab = () => {
|
||||
filterCustomer.length > 0
|
||||
? filterCustomer.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
sales:
|
||||
sales_id:
|
||||
filterSales.length > 0
|
||||
? filterSales.map((v) => String(v.value)).join(',')
|
||||
: undefined,
|
||||
@@ -185,7 +219,7 @@ const CustomerPaymentTab = () => {
|
||||
|
||||
const response = await FinanceApi.getCustomerPaymentReport(
|
||||
params.customer_id,
|
||||
params.sales,
|
||||
params.sales_id,
|
||||
params.filter_by,
|
||||
params.start_date,
|
||||
params.end_date,
|
||||
@@ -260,27 +294,6 @@ const CustomerPaymentTab = () => {
|
||||
}
|
||||
}, [customerPaymentExport]);
|
||||
|
||||
// ===== 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 = (
|
||||
summary: CustomerPaymentSummary
|
||||
): ColumnDef<CustomerPaymentReport['rows'][0]>[] => {
|
||||
@@ -532,14 +545,37 @@ const CustomerPaymentTab = () => {
|
||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||
>
|
||||
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||
<Button variant='outline' onClick={filterModal.openModal}>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={filterModal.openModal}
|
||||
className={
|
||||
hasFilters
|
||||
? 'bg-linear-to-b from-[#0069E0]/40 to-white text-[#0069E0] rounded-lg'
|
||||
: 'rounded-lg'
|
||||
}
|
||||
>
|
||||
<Icon icon='heroicons:funnel' width={18} height={18} />
|
||||
Filter
|
||||
{hasFilters && (
|
||||
<Badge
|
||||
variant='default'
|
||||
className={{
|
||||
badge:
|
||||
'rounded-lg px-1.5 py-2.5 text-xs font-semibold bg-error text-white',
|
||||
}}
|
||||
>
|
||||
{activeFiltersCount}
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Dropdown
|
||||
trigger={
|
||||
<Button variant='outline' isLoading={isAnyExportLoading}>
|
||||
<Button
|
||||
variant='outline'
|
||||
isLoading={isAnyExportLoading}
|
||||
className='rounded-lg'
|
||||
>
|
||||
<Icon
|
||||
icon='heroicons:cloud-arrow-down'
|
||||
width={18}
|
||||
@@ -550,7 +586,7 @@ const CustomerPaymentTab = () => {
|
||||
}
|
||||
align='end'
|
||||
>
|
||||
<Menu>
|
||||
<Menu className={'w-full'}>
|
||||
<MenuItem title='Excel' onClick={handleExportExcel} />
|
||||
<MenuItem title='PDF' onClick={handleExportPdf} />
|
||||
</Menu>
|
||||
@@ -608,10 +644,9 @@ const CustomerPaymentTab = () => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectInput
|
||||
<SelectInputCheckbox
|
||||
label='Customer'
|
||||
placeholder='Pilih Customer'
|
||||
isMulti
|
||||
options={customerOptions}
|
||||
value={filterCustomer}
|
||||
onChange={(val) => {
|
||||
@@ -621,21 +656,23 @@ const CustomerPaymentTab = () => {
|
||||
}}
|
||||
isLoading={isLoadingCustomers}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreCustomers}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SelectInput
|
||||
<SelectInputCheckbox
|
||||
label='Sales'
|
||||
placeholder='Pilih Sales'
|
||||
isMulti
|
||||
options={salesOptions}
|
||||
value={filterSales}
|
||||
onChange={(val) => {
|
||||
setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
|
||||
}}
|
||||
isLoading={isLoadingSales}
|
||||
isClearable
|
||||
onMenuScrollToBottom={loadMoreSales}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -704,8 +741,12 @@ const CustomerPaymentTab = () => {
|
||||
<Card
|
||||
key={customerReport.customer.id}
|
||||
title={customerReport.customer.name}
|
||||
subtitle={`${customerReport.customer.address || ''}`}
|
||||
className={{ wrapper: 'w-full' }}
|
||||
className={{
|
||||
wrapper: 'w-full rounded-2xl',
|
||||
body: 'p-0',
|
||||
title:
|
||||
'py-1.5 px-3 bg-[#0069E0] text-white text-lg font-normal',
|
||||
}}
|
||||
variant='bordered'
|
||||
collapsible={true}
|
||||
>
|
||||
@@ -716,7 +757,7 @@ const CustomerPaymentTab = () => {
|
||||
renderFooter={customerReport.rows.length > 0}
|
||||
className={{
|
||||
containerClassName: 'w-full',
|
||||
tableWrapperClassName: 'overflow-x-auto mt-4',
|
||||
tableWrapperClassName: 'overflow-x-auto',
|
||||
tableClassName: 'w-full table-auto text-sm',
|
||||
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
|
||||
headerColumnClassName:
|
||||
@@ -738,20 +779,6 @@ const CustomerPaymentTab = () => {
|
||||
})
|
||||
)}
|
||||
</Card>
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export class FinanceApiService extends BaseApiService<
|
||||
|
||||
async getCustomerPaymentReport(
|
||||
customer_id?: string,
|
||||
sales?: string,
|
||||
sales_id?: string,
|
||||
filter_by?: 'do_date',
|
||||
start_date?: string,
|
||||
end_date?: string,
|
||||
@@ -27,7 +27,7 @@ export class FinanceApiService extends BaseApiService<
|
||||
method: 'GET',
|
||||
params: {
|
||||
customer_id: customer_id,
|
||||
sales: sales,
|
||||
sales_id: sales_id,
|
||||
filter_by: filter_by,
|
||||
start_date: start_date,
|
||||
end_date: end_date,
|
||||
|
||||
Reference in New Issue
Block a user