Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into fix/dashboard

This commit is contained in:
randy-ar
2026-01-14 15:37:44 +07:00
11 changed files with 434 additions and 279 deletions
+6 -2
View File
@@ -148,7 +148,11 @@ const Card = ({
const hasContent = children || actions || footer; const hasContent = children || actions || footer;
const titleContent = ( 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'> <div className='flex-1'>
{title && <h2 className={getTitleClasses()}>{title}</h2>} {title && <h2 className={getTitleClasses()}>{title}</h2>}
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>} {subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
@@ -156,7 +160,7 @@ const Card = ({
{collapsible && ( {collapsible && (
<button <button
onClick={() => handleCollapsedChange(!isCollapsed)} 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'} aria-label={isCollapsed ? 'Expand content' : 'Collapse content'}
> >
<Icon <Icon
+133 -26
View File
@@ -9,15 +9,20 @@ import Select, {
SingleValue, SingleValue,
components as ReactSelectComponents, components as ReactSelectComponents,
ControlProps, ControlProps,
MenuListProps,
} from 'react-select'; } from 'react-select';
import CreatableSelect from 'react-select/creatable'; import CreatableSelect from 'react-select/creatable';
import makeAnimated from 'react-select/animated'; import makeAnimated from 'react-select/animated';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { cn, getByPath } from '@/lib/helper'; import { cn, getByPath } from '@/lib/helper';
import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite';
import { httpClientFetcher } from '@/services/http/client'; import { httpClientFetcher } from '@/services/http/client';
import { BaseApiResponse } from '@/types/api/api-general'; import {
import { isResponseSuccess } from '@/lib/api-helper'; BaseApiResponse,
ErrorApiResponse,
SuccessApiResponse,
} from '@/types/api/api-general';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
export interface OptionType { export interface OptionType {
value: string | number; value: string | number;
@@ -35,6 +40,7 @@ interface SelectInputBaseProps<T = OptionType> {
bottomLabel?: ReactNode; bottomLabel?: ReactNode;
options: T[]; options: T[];
optionComponent?: OptionComponent<T>; optionComponent?: OptionComponent<T>;
components?: Partial<typeof ReactSelectComponents>;
isDisabled?: boolean; isDisabled?: boolean;
isLoading?: boolean; isLoading?: boolean;
isClearable?: boolean; isClearable?: boolean;
@@ -56,9 +62,13 @@ interface SelectInputBaseProps<T = OptionType> {
onInputChange?: (search: string) => void; onInputChange?: (search: string) => void;
startAdornment?: ReactNode; startAdornment?: ReactNode;
menuPortalTarget?: HTMLElement | null; 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; createables?: boolean;
value?: T | T[] | null; value?: T | T[] | null;
onChange?: (val: T | T[] | null) => void; 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 SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const { const {
label, label,
@@ -101,6 +134,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
onChange, onChange,
options, options,
optionComponent, optionComponent,
components: customComponents,
isDisabled, isDisabled,
isLoading, isLoading,
isClearable, isClearable,
@@ -119,6 +153,9 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
onInputChange, onInputChange,
startAdornment, startAdornment,
menuPortalTarget, menuPortalTarget,
closeMenuOnSelect,
hideSelectedOptions,
onMenuScrollToBottom,
} = props; } = props;
const [internalInputValue, setInternalInputValue] = useState(''); const [internalInputValue, setInternalInputValue] = useState('');
@@ -128,14 +165,18 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
const components = useMemo(() => { const components = useMemo(() => {
const base = isAnimated ? animatedComponents : {}; const base = isAnimated ? animatedComponents : {};
const customComponents = { ...base, IndicatorSeparator: () => null }; const mergedComponents = { ...base, IndicatorSeparator: () => null };
if (startAdornment) { if (startAdornment) {
customComponents.Control = CustomControl; mergedComponents.Control = CustomControl;
} }
return customComponents; if (customComponents) {
}, [isAnimated, startAdornment]); Object.assign(mergedComponents, customComponents);
}
return mergedComponents;
}, [isAnimated, startAdornment, customComponents]);
const internalInputChangeHandler = (val: string, meta: InputActionMeta) => { const internalInputChangeHandler = (val: string, meta: InputActionMeta) => {
if (meta.action === 'input-change') setInternalInputValue(val); if (meta.action === 'input-change') setInternalInputValue(val);
@@ -205,6 +246,8 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
isRtl={isRtl} isRtl={isRtl}
isSearchable={isSearchable} isSearchable={isSearchable}
placeholder={placeholder} placeholder={placeholder}
closeMenuOnSelect={closeMenuOnSelect}
hideSelectedOptions={hideSelectedOptions}
className={cn('w-full', className?.select)} className={cn('w-full', className?.select)}
classNames={{ classNames={{
...(!startAdornment && { ...(!startAdornment && {
@@ -256,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,
@@ -269,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>}
@@ -288,34 +333,96 @@ const useSelect = <T,>(
) => { ) => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const optionsUrlParams = useMemo(() => { const pageKey = 'page';
return new URLSearchParams({ const limitKey = 'limit';
const limit = params?.['limit'] ?? 10;
const getKey = (
pageIndex: number,
previousPageData?: BaseApiResponse<T[]>
) => {
// stop when backend says no more pages
if (previousPageData && isResponseSuccess(previousPageData)) {
const meta = previousPageData.meta;
if (meta && meta.page >= meta.total_pages) return null;
}
const qs = new URLSearchParams({
...(params ?? {}),
[searchKey]: inputValue ?? '', [searchKey]: inputValue ?? '',
...params, [pageKey]: String(pageIndex + 1),
[limitKey]: String(limit),
}).toString(); }).toString();
}, [inputValue, searchKey, params]);
const optionsUrl = `${basePath}?${optionsUrlParams}`; return `${basePath}?${qs}`;
};
const { data, isLoading } = useSWR(optionsUrl, async (url) => { const {
return await httpClientFetcher<BaseApiResponse<T[]>>(url); data: pages,
}); isLoading,
isValidating,
size,
setSize,
} = useSWRInfinite<BaseApiResponse<T[]>>(getKey, (url) =>
httpClientFetcher<BaseApiResponse<T[]>>(url)
);
const options = isResponseSuccess(data) const options = useMemo(() => {
? data.data.map((item) => { if (!pages) return [];
return {
value: getByPath<T, number>(item, valueKey as string), return pages.flatMap((page) =>
label: getByPath<T, string>(item, labelKey as string), isResponseSuccess(page)
}; ? page.data.map((item) => ({
}) value: getByPath<T, number>(item, valueKey as string),
: []; label: getByPath<T, string>(item, labelKey as string),
}))
: []
);
}, [pages, valueKey, labelKey]);
const lastPage = pages?.[pages.length - 1];
const hasMore =
!!lastPage &&
isResponseSuccess(lastPage) &&
!!lastPage.meta &&
lastPage.meta.page < lastPage.meta.total_pages;
const loadMore = () => {
if (!hasMore) return;
setSize(size + 1);
};
let formattedSuccessRawData: SuccessApiResponse<T[]> | undefined = undefined;
let formattedErrorRawData: ErrorApiResponse | undefined = undefined;
const latestPagesIndex = pages?.length ? pages.length - 1 : 0;
if (isResponseSuccess(pages?.[latestPagesIndex])) {
formattedSuccessRawData = {
...pages?.[latestPagesIndex],
data:
pages?.flatMap((page) => (isResponseSuccess(page) ? page.data : [])) ??
[],
};
}
if (isResponseError(pages?.[latestPagesIndex])) {
formattedErrorRawData = pages?.[latestPagesIndex];
}
return { return {
inputValue, inputValue,
setInputValue, setInputValue,
options, options,
isLoadingOptions: isLoading, rawData: isResponseSuccess(pages?.[latestPagesIndex])
rawData: data, ? formattedSuccessRawData
: formattedErrorRawData,
isLoadingOptions: isLoading || isValidating,
isLoadingMore: isValidating && size > 1,
hasMore,
loadMore,
}; };
}; };
@@ -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 { 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)
} }
@@ -7,9 +7,11 @@ import SelectInput, {
useSelect, useSelect,
OptionType, OptionType,
} from '@/components/input/SelectInput'; } from '@/components/input/SelectInput';
import SelectInputCheckbox from '@/components/input/SelectInputCheckbox';
import DateInput from '@/components/input/DateInput'; import DateInput from '@/components/input/DateInput';
import { CustomerApi } from '@/services/api/master-data'; import { CustomerApi } from '@/services/api/master-data';
import { FinanceApi } from '@/services/api/report/finance-report'; import { FinanceApi } from '@/services/api/report/finance-report';
import { UserApi } from '@/services/api/user';
import Table from '@/components/Table'; import Table from '@/components/Table';
import { ColumnDef } from '@tanstack/react-table'; import { ColumnDef } from '@tanstack/react-table';
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper'; import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
@@ -18,7 +20,6 @@ import {
CustomerPaymentSummary, CustomerPaymentSummary,
} from '@/types/api/report/customer-payment'; } from '@/types/api/report/customer-payment';
import { isResponseSuccess } from '@/lib/api-helper'; import { isResponseSuccess } from '@/lib/api-helper';
import Pagination from '@/components/Pagination';
import Button from '@/components/Button'; import Button from '@/components/Button';
import Dropdown from '@/components/Dropdown'; import Dropdown from '@/components/Dropdown';
import MenuItem from '@/components/menu/MenuItem'; import MenuItem from '@/components/menu/MenuItem';
@@ -37,31 +38,34 @@ const CustomerPaymentTab = () => {
// ===== PAGINATION STATE ===== // ===== PAGINATION STATE =====
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10); const [pageSize] = useState(10);
// ===== SUBMISSION STATE ===== // ===== SUBMISSION STATE =====
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
// ===== FILTER STATE ===== // ===== FILTER STATE =====
const [filterCustomer, setFilterCustomer] = useState<OptionType[]>([]); const [filterCustomer, setFilterCustomer] = useState<typeof customerOptions>(
const [filterSales, setFilterSales] = useState<OptionType[]>([]); []
);
const [filterSales, setFilterSales] = useState<typeof salesOptions>([]);
const [filterStartDate, setFilterStartDate] = useState(''); const [filterStartDate, setFilterStartDate] = useState('');
const [filterEndDate, setFilterEndDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState('');
const filterModal = useModal(); const filterModal = useModal();
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } = const {
useSelect(CustomerApi.basePath, 'id', 'name', 'search'); options: customerOptions,
isLoadingOptions: isLoadingCustomers,
loadMore: loadMoreCustomers,
hasMore: hasMoreCustomers,
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
const salesOptions = useMemo( const {
() => [ options: salesOptions,
{ value: 'Sales A', label: 'Sales A' }, isLoadingOptions: isLoadingSales,
{ value: 'Sales B', label: 'Sales B' }, loadMore: loadMoreSales,
{ value: 'Sales C', label: 'Sales C' }, hasMore: hasMoreSales,
// TODO: Fetch sales options from API } = useSelect(UserApi.basePath, 'id', 'name', 'search');
],
[]
);
const dataTypeOptions = useMemo( const dataTypeOptions = useMemo(
() => [{ value: 'do_date', label: 'Tanggal Jual' }], () => [{ value: 'do_date', label: 'Tanggal Jual' }],
@@ -115,6 +119,41 @@ const CustomerPaymentTab = () => {
filterModal.closeModal(); filterModal.closeModal();
}, [filterModal]); }, [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 ===== // ===== DATA FETCHING =====
const { data: customerPayment, isLoading } = useSWR( const { data: customerPayment, isLoading } = useSWR(
isSubmitted isSubmitted
@@ -124,7 +163,7 @@ const CustomerPaymentTab = () => {
filterCustomer.length > 0 filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
sales: sales_id:
filterSales.length > 0 filterSales.length > 0
? filterSales.map((v) => String(v.value)).join(',') ? filterSales.map((v) => String(v.value)).join(',')
: undefined, : undefined,
@@ -141,7 +180,7 @@ const CustomerPaymentTab = () => {
([, params]) => ([, params]) =>
FinanceApi.getCustomerPaymentReport( FinanceApi.getCustomerPaymentReport(
params.customer_id, params.customer_id,
params.sales, params.sales_id,
params.filter_by, params.filter_by,
params.start_date, params.start_date,
params.end_date, params.end_date,
@@ -158,11 +197,6 @@ const CustomerPaymentTab = () => {
[customerPayment] [customerPayment]
); );
const meta =
isResponseSuccess(customerPayment) && customerPayment?.meta
? customerPayment.meta
: null;
// ===== EXPORT DATA FETCHER ===== // ===== EXPORT DATA FETCHER =====
const customerPaymentExport = useCallback(async (): Promise< const customerPaymentExport = useCallback(async (): Promise<
CustomerPaymentReport[] | null CustomerPaymentReport[] | null
@@ -172,7 +206,7 @@ const CustomerPaymentTab = () => {
filterCustomer.length > 0 filterCustomer.length > 0
? filterCustomer.map((v) => String(v.value)).join(',') ? filterCustomer.map((v) => String(v.value)).join(',')
: undefined, : undefined,
sales: sales_id:
filterSales.length > 0 filterSales.length > 0
? filterSales.map((v) => String(v.value)).join(',') ? filterSales.map((v) => String(v.value)).join(',')
: undefined, : undefined,
@@ -185,7 +219,7 @@ const CustomerPaymentTab = () => {
const response = await FinanceApi.getCustomerPaymentReport( const response = await FinanceApi.getCustomerPaymentReport(
params.customer_id, params.customer_id,
params.sales, params.sales_id,
params.filter_by, params.filter_by,
params.start_date, params.start_date,
params.end_date, params.end_date,
@@ -260,27 +294,6 @@ const CustomerPaymentTab = () => {
} }
}, [customerPaymentExport]); }, [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 = ( const getTableColumns = (
summary: CustomerPaymentSummary summary: CustomerPaymentSummary
): ColumnDef<CustomerPaymentReport['rows'][0]>[] => { ): ColumnDef<CustomerPaymentReport['rows'][0]>[] => {
@@ -532,14 +545,37 @@ const CustomerPaymentTab = () => {
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}> <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} /> <Icon icon='heroicons:funnel' width={18} height={18} />
Filter 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> </Button>
<Dropdown <Dropdown
trigger={ trigger={
<Button variant='outline' isLoading={isAnyExportLoading}> <Button
variant='outline'
isLoading={isAnyExportLoading}
className='rounded-lg'
>
<Icon <Icon
icon='heroicons:cloud-arrow-down' icon='heroicons:cloud-arrow-down'
width={18} width={18}
@@ -550,7 +586,7 @@ const CustomerPaymentTab = () => {
} }
align='end' align='end'
> >
<Menu> <Menu className={'w-full'}>
<MenuItem title='Excel' onClick={handleExportExcel} /> <MenuItem title='Excel' onClick={handleExportExcel} />
<MenuItem title='PDF' onClick={handleExportPdf} /> <MenuItem title='PDF' onClick={handleExportPdf} />
</Menu> </Menu>
@@ -608,10 +644,9 @@ const CustomerPaymentTab = () => {
</div> </div>
<div> <div>
<SelectInput <SelectInputCheckbox
label='Customer' label='Customer'
placeholder='Pilih Customer' placeholder='Pilih Customer'
isMulti
options={customerOptions} options={customerOptions}
value={filterCustomer} value={filterCustomer}
onChange={(val) => { onChange={(val) => {
@@ -621,21 +656,23 @@ const CustomerPaymentTab = () => {
}} }}
isLoading={isLoadingCustomers} isLoading={isLoadingCustomers}
isClearable isClearable
onMenuScrollToBottom={loadMoreCustomers}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
</div> </div>
<div> <div>
<SelectInput <SelectInputCheckbox
label='Sales' label='Sales'
placeholder='Pilih Sales' placeholder='Pilih Sales'
isMulti
options={salesOptions} options={salesOptions}
value={filterSales} value={filterSales}
onChange={(val) => { onChange={(val) => {
setFilterSales(Array.isArray(val) ? val : val ? [val] : []); setFilterSales(Array.isArray(val) ? val : val ? [val] : []);
}} }}
isLoading={isLoadingSales}
isClearable isClearable
onMenuScrollToBottom={loadMoreSales}
className={{ wrapper: 'w-full' }} className={{ wrapper: 'w-full' }}
/> />
</div> </div>
@@ -704,8 +741,12 @@ const CustomerPaymentTab = () => {
<Card <Card
key={customerReport.customer.id} key={customerReport.customer.id}
title={customerReport.customer.name} title={customerReport.customer.name}
subtitle={`${customerReport.customer.address || ''}`} className={{
className={{ wrapper: 'w-full' }} wrapper: 'w-full rounded-2xl',
body: 'p-0',
title:
'py-1.5 px-3 bg-[#0069E0] text-white text-lg font-normal',
}}
variant='bordered' variant='bordered'
collapsible={true} collapsible={true}
> >
@@ -716,7 +757,7 @@ const CustomerPaymentTab = () => {
renderFooter={customerReport.rows.length > 0} renderFooter={customerReport.rows.length > 0}
className={{ className={{
containerClassName: 'w-full', containerClassName: 'w-full',
tableWrapperClassName: 'overflow-x-auto mt-4', tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto text-sm', tableClassName: 'w-full table-auto text-sm',
headerRowClassName: 'border-b border-b-gray-200 bg-gray-50', headerRowClassName: 'border-b border-b-gray-200 bg-gray-50',
headerColumnClassName: headerColumnClassName:
@@ -738,20 +779,6 @@ const CustomerPaymentTab = () => {
}) })
)} )}
</Card> </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> </div>
); );
}; };
+2 -2
View File
@@ -14,7 +14,7 @@ export class FinanceApiService extends BaseApiService<
async getCustomerPaymentReport( async getCustomerPaymentReport(
customer_id?: string, customer_id?: string,
sales?: string, sales_id?: string,
filter_by?: 'do_date', filter_by?: 'do_date',
start_date?: string, start_date?: string,
end_date?: string, end_date?: string,
@@ -27,7 +27,7 @@ export class FinanceApiService extends BaseApiService<
method: 'GET', method: 'GET',
params: { params: {
customer_id: customer_id, customer_id: customer_id,
sales: sales, sales_id: sales_id,
filter_by: filter_by, filter_by: filter_by,
start_date: start_date, start_date: start_date,
end_date: end_date, end_date: end_date,