mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +00:00
Merge branch 'development' into 'staging'
Development See merge request mbugroup/lti-web-client!182
This commit is contained in:
@@ -38,9 +38,11 @@ const ExpenseEditPage = () => {
|
|||||||
!isLoadingExpense &&
|
!isLoadingExpense &&
|
||||||
isResponseSuccess(expense) &&
|
isResponseSuccess(expense) &&
|
||||||
expense.data.latest_approval.step_number !== 5 &&
|
expense.data.latest_approval.step_number !== 5 &&
|
||||||
|
expense.data.latest_approval.step_number !== 6 &&
|
||||||
(expense.data.latest_approval.step_number === 1 ||
|
(expense.data.latest_approval.step_number === 1 ||
|
||||||
expense.data.latest_approval.step_number === 2 ||
|
expense.data.latest_approval.step_number === 2 ||
|
||||||
expense.data.latest_approval.step_number === 3);
|
expense.data.latest_approval.step_number === 3 ||
|
||||||
|
expense.data.latest_approval.step_number === 4);
|
||||||
|
|
||||||
if (!isLoadingExpense && !isExpenseCanBeEdited) {
|
if (!isLoadingExpense && !isExpenseCanBeEdited) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Button, { ButtonProps } from '@/components/Button';
|
||||||
|
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
||||||
|
import { cn } from '@/lib/helper';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { FormikValues } from 'formik';
|
||||||
|
|
||||||
|
export type ButtonFilterProps = ButtonProps & {
|
||||||
|
values: FormikValues;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ButtonFilter = ({ values, onClick, ...props }: ButtonFilterProps) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
onClick={onClick}
|
||||||
|
className={cn(
|
||||||
|
getFilledFormikValuesCount(values) > 0
|
||||||
|
? 'bg-gradient-to-t from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
||||||
|
: '',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:funnel'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className={
|
||||||
|
getFilledFormikValuesCount(values) > 0 ? 'text-blue-600' : ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Filter
|
||||||
|
{getFilledFormikValuesCount(values) > 0 && (
|
||||||
|
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
|
||||||
|
{getFilledFormikValuesCount(values)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ButtonFilter;
|
||||||
@@ -18,7 +18,7 @@ const AlertErrorList = ({
|
|||||||
if (formErrorList.length === 0) return null;
|
if (formErrorList.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert color='error' className='w-full flex flex-col gap-2 px-4 m-4'>
|
<Alert color='error' className='w-full flex flex-col gap-2 px-4'>
|
||||||
<div className='flex justify-between items-center gap-2 w-full'>
|
<div className='flex justify-between items-center gap-2 w-full'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
<Icon icon='material-symbols:error-outline' width={24} height={24} />
|
||||||
|
|||||||
@@ -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>}
|
||||||
@@ -280,7 +325,7 @@ const SelectInput = <T extends OptionType>(props: SelectInputProps<T>) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useSelect = <T,>(
|
const useSelect = <T,>(
|
||||||
basePath: string,
|
basePath: string | null,
|
||||||
valueKey: keyof T | string,
|
valueKey: keyof T | string,
|
||||||
labelKey: keyof T | string,
|
labelKey: keyof T | string,
|
||||||
searchKey: string = 'search',
|
searchKey: string = 'search',
|
||||||
@@ -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 ? `${basePath}?${qs}` : null;
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
HppPurchaseData,
|
HppPurchaseData,
|
||||||
ProfitLossDataAmount,
|
ProfitLossDataAmount,
|
||||||
} from '@/types/api/closing';
|
} from '@/types/api/closing';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
type HppTableRow =
|
type HppTableRow =
|
||||||
@@ -55,9 +56,16 @@ const ClosingFinanceTable = ({
|
|||||||
}: {
|
}: {
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: finance, isLoading } = useSWR(
|
const { data: finance, isLoading } = useSWR(
|
||||||
`/closing/finance/${projectFlockId}`,
|
`/closing/finance/${projectFlockId}${kandangId ? `/${kandangId}` : ''}`,
|
||||||
() => ClosingApi.getFinance(projectFlockId)
|
() =>
|
||||||
|
ClosingApi.getFinance(
|
||||||
|
projectFlockId,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const staticHppRows: Array<{
|
const staticHppRows: Array<{
|
||||||
@@ -283,6 +291,7 @@ const ClosingFinanceTable = ({
|
|||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<HppTableRow>
|
<Table<HppTableRow>
|
||||||
data={hppTableData}
|
data={hppTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'No.',
|
header: 'No.',
|
||||||
@@ -299,7 +308,7 @@ const ClosingFinanceTable = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Type',
|
header: 'Jenis',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
||||||
},
|
},
|
||||||
@@ -455,6 +464,7 @@ const ClosingFinanceTable = ({
|
|||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<ProfitLossTableRow>
|
<Table<ProfitLossTableRow>
|
||||||
data={profitLossTableData}
|
data={profitLossTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Jenis',
|
header: 'Jenis',
|
||||||
|
|||||||
@@ -5,21 +5,27 @@ import { cn, formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
|||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { Overhead, OverheadTotal } from '@/types/api/closing';
|
import { Overhead, OverheadTotal } from '@/types/api/closing';
|
||||||
import { ColumnDef } from '@tanstack/react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
interface ClosingOverheadTableProps {
|
interface ClosingOverheadTableProps {
|
||||||
type?: 'detail';
|
|
||||||
projectFlockId: number;
|
projectFlockId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClosingOverheadTable = ({
|
const ClosingOverheadTable = ({
|
||||||
type,
|
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingOverheadTableProps) => {
|
}: ClosingOverheadTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: overhead, isLoading: isLoadingOverhead } = useSWR(
|
const { data: overhead, isLoading: isLoadingOverhead } = useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/overhead`,
|
`${ClosingApi.basePath}/${projectFlockId}${kandangId ? `/${kandangId}` : ''}/overhead`,
|
||||||
() => ClosingApi.getOverhead(projectFlockId),
|
() =>
|
||||||
|
ClosingApi.getOverhead(
|
||||||
|
projectFlockId,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
),
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
@@ -148,6 +154,7 @@ const ClosingOverheadTable = ({
|
|||||||
'whitespace-nowrap'
|
'whitespace-nowrap'
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
isLoading={isLoadingOverhead}
|
||||||
renderFooter={
|
renderFooter={
|
||||||
isResponseSuccess(overhead)
|
isResponseSuccess(overhead)
|
||||||
? overhead.data?.overheads.length > 0
|
? overhead.data?.overheads.length > 0
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ const ClosingProductionDataTabContent = ({
|
|||||||
value={formatNumber(performance.mor_diff)}
|
value={formatNumber(performance.mor_diff)}
|
||||||
unitClassName='hidden'
|
unitClassName='hidden'
|
||||||
/>
|
/>
|
||||||
<DataRow
|
{/* <DataRow
|
||||||
label='AWG Std'
|
label='AWG Std'
|
||||||
value={formatNumber(performance.awg_std)}
|
value={formatNumber(performance.awg_std)}
|
||||||
unit='Gr/Hari'
|
unit='Gr/Hari'
|
||||||
@@ -206,7 +206,7 @@ const ClosingProductionDataTabContent = ({
|
|||||||
label='AWG Act'
|
label='AWG Act'
|
||||||
value={formatNumber(performance.awg_act)}
|
value={formatNumber(performance.awg_act)}
|
||||||
unit='Gr/Hari'
|
unit='Gr/Hari'
|
||||||
/>
|
/> */}
|
||||||
<DataRow
|
<DataRow
|
||||||
label='Feed Intake Std'
|
label='Feed Intake Std'
|
||||||
value={formatNumber(performance.feed_intake_std)}
|
value={formatNumber(performance.feed_intake_std)}
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ const ClosingsTable = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
@@ -228,6 +229,7 @@ const ClosingsTable = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6',
|
wrapper: 'col-span-12 sm:col-span-6',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import SelectInput, {
|
|||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { DashboardApi } from '@/services/api/dashboard';
|
import { DashboardApi } from '@/services/api/dashboard';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
@@ -30,6 +30,11 @@ import DashboardStats from '@/components/pages/dashboard/chart/DashboardStats';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -49,6 +54,7 @@ const DashboardProduction = () => {
|
|||||||
);
|
);
|
||||||
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
||||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
|
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -64,22 +70,32 @@ const DashboardProduction = () => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// ===== SELECT =====
|
// ===== SELECT =====
|
||||||
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
|
|
||||||
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
|
||||||
limit: 'limit',
|
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
|
||||||
});
|
|
||||||
const {
|
const {
|
||||||
|
setInputValue: setInputValueFlock,
|
||||||
|
options: flockOptions,
|
||||||
|
isLoadingOptions: isLoadingFlockOptions,
|
||||||
|
loadMore: loadMoreFlock,
|
||||||
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
||||||
|
limit: 'limit',
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
setInputValue: setInputValueLocation,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocation,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
const { options: kandangOptions, isLoadingOptions: isLoadingKandangOptions } =
|
const {
|
||||||
useSelect(KandangApi.basePath, 'id', 'name', '', {
|
setInputValue: setInputValueKandang,
|
||||||
limit: 'limit',
|
options: kandangOptions,
|
||||||
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
});
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name', '', {
|
||||||
|
limit: 'limit',
|
||||||
|
location_id: selectedLocationIds ? selectedLocationIds.toString() : '',
|
||||||
|
});
|
||||||
const comparisonTypeOptions = [
|
const comparisonTypeOptions = [
|
||||||
{ value: 'FARM', label: 'Farm' },
|
{ value: 'FARM', label: 'Farm' },
|
||||||
{ value: 'FLOCK', label: 'Flock' },
|
{ value: 'FLOCK', label: 'Flock' },
|
||||||
@@ -143,12 +159,27 @@ const DashboardProduction = () => {
|
|||||||
console.log(endpointUrl);
|
console.log(endpointUrl);
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
refreshDashboardProductionData();
|
refreshDashboardProductionData();
|
||||||
formik.resetForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
|
// ===== Export PDF =====
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
setExporting(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait for state to render, then trigger print
|
||||||
|
useEffect(() => {
|
||||||
|
if (exporting) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
window.print();
|
||||||
|
setExporting(false);
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [exporting]);
|
||||||
|
|
||||||
if (isLoadingDashboardProductionData) {
|
if (isLoadingDashboardProductionData) {
|
||||||
return (
|
return (
|
||||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||||
@@ -156,71 +187,39 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full p-4 space-y-6'>
|
<section className='w-full p-4 space-y-6'>
|
||||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
<div className='flex flex-row justify-end gap-2'>
|
<div className='flex flex-row justify-end gap-2'>
|
||||||
<Button
|
<ButtonFilter
|
||||||
|
values={{
|
||||||
|
...formik.values,
|
||||||
|
analysisMode: undefined,
|
||||||
|
}}
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className={`min-w-28 rounded-lg ${
|
|
||||||
isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters
|
|
||||||
? 'bg-gradient-to-r from-blue-50 to-blue-100 border-blue-500 text-blue-600 hover:from-blue-100 hover:to-blue-200'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
onClick={() => filterModal.openModal()}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='heroicons:funnel'
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className={
|
|
||||||
isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters
|
|
||||||
? 'text-blue-600'
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
Filter
|
|
||||||
{isResponseSuccess(dashboardProductionResponse) &&
|
|
||||||
dashboardProductionResponse.meta &&
|
|
||||||
(dashboardProductionResponse.meta as unknown as DashboardMeta)
|
|
||||||
.filters && (
|
|
||||||
<span className='w-6 h-6 text-white bg-red-500 rounded-lg flex items-center justify-center text-xs'>
|
|
||||||
{(() => {
|
|
||||||
const meta =
|
|
||||||
dashboardProductionResponse.meta as unknown as DashboardMeta;
|
|
||||||
if (!meta.filters) return 0;
|
|
||||||
const count =
|
|
||||||
(meta.filters.location_ids.length > 1
|
|
||||||
? meta.filters.location_ids.length
|
|
||||||
: 0) +
|
|
||||||
(meta.filters.flock_ids.length > 1
|
|
||||||
? meta.filters.flock_ids.length
|
|
||||||
: 0) +
|
|
||||||
(meta.filters.kandang_ids.length > 1
|
|
||||||
? meta.filters.kandang_ids.length
|
|
||||||
: 0);
|
|
||||||
return meta.filters.analysis_mode === 'OVERVIEW'
|
|
||||||
? 1
|
|
||||||
: count;
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='outline'
|
|
||||||
color='neutral'
|
|
||||||
className='min-w-28 rounded-lg'
|
className='min-w-28 rounded-lg'
|
||||||
|
onClick={() => filterModal.openModal()}
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
trigger={
|
||||||
|
<Button variant='outline' className='min-w-28 rounded-lg z-50'>
|
||||||
|
<Icon icon='heroicons:arrow-down-tray' />
|
||||||
|
Export
|
||||||
|
<Icon icon='heroicons:chevron-down' />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className={{
|
||||||
|
content: 'w-full',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
|
<Menu className={exporting ? 'hidden' : ''}>
|
||||||
Export
|
<MenuItem title='PDF' onClick={handleExportPDF} />
|
||||||
<Icon icon='heroicons:chevron-down' width={20} height={20} />
|
</Menu>
|
||||||
</Button>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -287,7 +286,7 @@ const DashboardProduction = () => {
|
|||||||
{/* Rentang Waktu */}
|
{/* Rentang Waktu */}
|
||||||
<div className='px-4'>
|
<div className='px-4'>
|
||||||
<label className='flex items-center gap-2 mb-3'>Tanggal</label>
|
<label className='flex items-center gap-2 mb-3'>Tanggal</label>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-start gap-2'>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='startDate'
|
name='startDate'
|
||||||
placeholder='Tanggal Mulai'
|
placeholder='Tanggal Mulai'
|
||||||
@@ -302,7 +301,7 @@ const DashboardProduction = () => {
|
|||||||
Boolean(formik.touched.startDate)
|
Boolean(formik.touched.startDate)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className='hidden md:block text-center'>—</span>
|
<div className='hidden md:block mt-3 text-center'>—</div>
|
||||||
<DateInput
|
<DateInput
|
||||||
name='endDate'
|
name='endDate'
|
||||||
placeholder='Tanggal Akhir'
|
placeholder='Tanggal Akhir'
|
||||||
@@ -383,6 +382,8 @@ const DashboardProduction = () => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
label='Farm'
|
label='Farm'
|
||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
|
onInputChange={setInputValueLocation}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
onChange={(selected) => {
|
onChange={(selected) => {
|
||||||
formik.setFieldValue('location', selected);
|
formik.setFieldValue('location', selected);
|
||||||
// Update selectedLocationIds for kandang filter
|
// Update selectedLocationIds for kandang filter
|
||||||
@@ -422,6 +423,8 @@ const DashboardProduction = () => {
|
|||||||
formik.setFieldValue('flock', selected)
|
formik.setFieldValue('flock', selected)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.flock as string}
|
errorMessage={formik.errors.flock as string}
|
||||||
|
onInputChange={setInputValueFlock}
|
||||||
|
onMenuScrollToBottom={loadMoreFlock}
|
||||||
options={flockOptions}
|
options={flockOptions}
|
||||||
isLoading={isLoadingFlockOptions}
|
isLoading={isLoadingFlockOptions}
|
||||||
isMulti={
|
isMulti={
|
||||||
@@ -450,6 +453,8 @@ const DashboardProduction = () => {
|
|||||||
formik.setFieldValue('kandang', selected)
|
formik.setFieldValue('kandang', selected)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.kandang as string}
|
errorMessage={formik.errors.kandang as string}
|
||||||
|
onInputChange={setInputValueKandang}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
options={kandangOptions}
|
options={kandangOptions}
|
||||||
isLoading={isLoadingKandangOptions}
|
isLoading={isLoadingKandangOptions}
|
||||||
isMulti={
|
isMulti={
|
||||||
@@ -465,7 +470,9 @@ const DashboardProduction = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<div className='w-full p-4'>
|
||||||
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
||||||
|
|||||||
@@ -283,261 +283,311 @@ const DashboardLineChart = ({
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chart */}
|
{/* Chart Container with Empty State Overlay */}
|
||||||
<ResponsiveContainer width='100%' height={350}>
|
<div className='relative'>
|
||||||
<LineChart
|
{/* Chart */}
|
||||||
data={(() => {
|
<ResponsiveContainer width='100%' height={350}>
|
||||||
// Transform data based on analysisMode
|
<LineChart
|
||||||
if (analysisMode === 'OVERVIEW') {
|
data={(() => {
|
||||||
// For OVERVIEW mode, use the selected chart data
|
// Transform data based on analysisMode
|
||||||
if (isOverviewCharts(data.charts)) {
|
if (analysisMode === 'OVERVIEW') {
|
||||||
const selectedChartData = data.charts[chartData];
|
// For OVERVIEW mode, use the selected chart data
|
||||||
if (!selectedChartData || !selectedChartData.dataset) return [];
|
if (isOverviewCharts(data.charts)) {
|
||||||
return selectedChartData.dataset;
|
const selectedChartData = data.charts[chartData];
|
||||||
|
if (!selectedChartData || !selectedChartData.dataset)
|
||||||
|
return [];
|
||||||
|
return selectedChartData.dataset;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
// For COMPARISON mode, use the first available comparison chart
|
||||||
|
if (isComparisonCharts(data.charts)) {
|
||||||
|
const chartData =
|
||||||
|
data.charts.location ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
|
||||||
|
if (!chartData || !chartData.dataset) return [];
|
||||||
|
return chartData.dataset;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
} else {
|
|
||||||
// For COMPARISON mode, use the first available comparison chart
|
|
||||||
if (isComparisonCharts(data.charts)) {
|
|
||||||
const chartData =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
|
|
||||||
if (!chartData || !chartData.dataset) return [];
|
|
||||||
return chartData.dataset;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
margin={{
|
|
||||||
top: 5,
|
|
||||||
right: 10,
|
|
||||||
left: 0,
|
|
||||||
bottom: 5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
|
||||||
<XAxis
|
|
||||||
dataKey='week'
|
|
||||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={{ stroke: '#e5e7eb' }}
|
|
||||||
label={{
|
|
||||||
value: 'Weeks',
|
|
||||||
position: 'insideBottom',
|
|
||||||
offset: -5,
|
|
||||||
style: { fontSize: 12, fill: '#9ca3af' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
|
||||||
tickLine={false}
|
|
||||||
axisLine={{ stroke: '#e5e7eb' }}
|
|
||||||
domain={(() => {
|
|
||||||
// Calculate dynamic domain based on visible data
|
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
|
||||||
let dataset: DashboardChartsDataset[] = [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
analysisMode === 'OVERVIEW' &&
|
|
||||||
isOverviewCharts(data.charts)
|
|
||||||
) {
|
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
dataset = data.charts[chartData]?.dataset || [];
|
|
||||||
} else if (
|
|
||||||
analysisMode === 'COMPARISON' &&
|
|
||||||
isComparisonCharts(data.charts)
|
|
||||||
) {
|
|
||||||
const comparisonChart =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
dataset = comparisonChart?.dataset || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all values from visible series
|
|
||||||
const visibleSeriesIds = Array.from(visibleSeries);
|
|
||||||
const allValues: number[] = [];
|
|
||||||
|
|
||||||
dataset.forEach((item: DashboardChartsDataset) => {
|
|
||||||
visibleSeriesIds.forEach((seriesId) => {
|
|
||||||
const value = item[seriesId];
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
allValues.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allValues.length === 0) return [0, 100];
|
|
||||||
|
|
||||||
const minValue = Math.min(...allValues);
|
|
||||||
const maxValue = Math.max(...allValues);
|
|
||||||
|
|
||||||
// Add padding (10% on each side)
|
|
||||||
const padding = (maxValue - minValue) * 0.1;
|
|
||||||
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
|
||||||
const domainMax = Math.ceil(maxValue + padding);
|
|
||||||
|
|
||||||
return [domainMin, domainMax];
|
|
||||||
})()}
|
})()}
|
||||||
ticks={(() => {
|
margin={{
|
||||||
// Calculate dynamic ticks based on domain
|
top: 5,
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
right: 10,
|
||||||
let dataset: DashboardChartsDataset[] = [];
|
left: 0,
|
||||||
|
bottom: 5,
|
||||||
if (
|
|
||||||
analysisMode === 'OVERVIEW' &&
|
|
||||||
isOverviewCharts(data.charts)
|
|
||||||
) {
|
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
dataset = data.charts[chartData]?.dataset || [];
|
|
||||||
} else if (
|
|
||||||
analysisMode === 'COMPARISON' &&
|
|
||||||
isComparisonCharts(data.charts)
|
|
||||||
) {
|
|
||||||
const comparisonChart =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
dataset = comparisonChart?.dataset || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleSeriesIds = Array.from(visibleSeries);
|
|
||||||
const allValues: number[] = [];
|
|
||||||
|
|
||||||
dataset.forEach((item: DashboardChartsDataset) => {
|
|
||||||
visibleSeriesIds.forEach((seriesId) => {
|
|
||||||
const value = item[seriesId];
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
allValues.push(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allValues.length === 0) return [0, 25, 50, 75, 100];
|
|
||||||
|
|
||||||
const minValue = Math.min(...allValues);
|
|
||||||
const maxValue = Math.max(...allValues);
|
|
||||||
const padding = (maxValue - minValue) * 0.1;
|
|
||||||
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
|
||||||
const domainMax = Math.ceil(maxValue + padding);
|
|
||||||
|
|
||||||
// Generate 5 evenly spaced ticks
|
|
||||||
const range = domainMax - domainMin;
|
|
||||||
const step = range / 4;
|
|
||||||
|
|
||||||
return [
|
|
||||||
domainMin,
|
|
||||||
Math.round(domainMin + step),
|
|
||||||
Math.round(domainMin + step * 2),
|
|
||||||
Math.round(domainMin + step * 3),
|
|
||||||
domainMax,
|
|
||||||
];
|
|
||||||
})()}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={{
|
|
||||||
backgroundColor: '#1f2937',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '8px 12px',
|
|
||||||
color: 'white',
|
|
||||||
}}
|
}}
|
||||||
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
>
|
||||||
itemStyle={{ color: 'white', fontSize: '12px' }}
|
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||||
labelFormatter={(value) => `Week ${value}`}
|
<XAxis
|
||||||
formatter={(
|
dataKey='week'
|
||||||
value: number | undefined,
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
name: string | undefined
|
tickLine={false}
|
||||||
) => {
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
if (value === undefined || name === undefined) return ['', ''];
|
label={{
|
||||||
|
value: 'Weeks',
|
||||||
|
position: 'insideBottom',
|
||||||
|
offset: -5,
|
||||||
|
style: { fontSize: 12, fill: '#9ca3af' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 11, fill: '#9ca3af' }}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={{ stroke: '#e5e7eb' }}
|
||||||
|
domain={(() => {
|
||||||
|
// Calculate dynamic domain based on visible data
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
// Get series data to find the unit
|
if (
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
analysisMode === 'OVERVIEW' &&
|
||||||
if (
|
isOverviewCharts(data.charts)
|
||||||
analysisMode === 'OVERVIEW' &&
|
) {
|
||||||
isOverviewCharts(data.charts)
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
) {
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
} else if (
|
||||||
} else if (
|
analysisMode === 'COMPARISON' &&
|
||||||
analysisMode === 'COMPARISON' &&
|
isComparisonCharts(data.charts)
|
||||||
isComparisonCharts(data.charts)
|
) {
|
||||||
) {
|
const comparisonChart =
|
||||||
const comparisonChart =
|
data.charts.location ||
|
||||||
data.charts.location ||
|
data.charts.flock ||
|
||||||
data.charts.flock ||
|
data.charts.kandang;
|
||||||
data.charts.kandang;
|
seriesData = comparisonChart?.series || [];
|
||||||
seriesData = comparisonChart?.series || [];
|
dataset = comparisonChart?.dataset || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the series that matches this line's name
|
// Get all values from visible series
|
||||||
const series = seriesData.find((s) => s.label === name);
|
const visibleSeriesIds = Array.from(visibleSeries);
|
||||||
const unit = series?.unit || '';
|
const allValues: number[] = [];
|
||||||
|
|
||||||
return [`${value} ${unit}`, name];
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
}}
|
visibleSeriesIds.forEach((seriesId) => {
|
||||||
/>
|
const value = item[seriesId];
|
||||||
{/* Dynamic Line rendering based on visible series */}
|
if (typeof value === 'number') {
|
||||||
{(() => {
|
allValues.push(value);
|
||||||
let seriesData: DashboardChartsSeries[] = [];
|
|
||||||
|
|
||||||
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
|
||||||
seriesData = data.charts[chartData]?.series || [];
|
|
||||||
} else if (
|
|
||||||
analysisMode === 'COMPARISON' &&
|
|
||||||
isComparisonCharts(data.charts)
|
|
||||||
) {
|
|
||||||
const comparisonChart =
|
|
||||||
data.charts.location ||
|
|
||||||
data.charts.flock ||
|
|
||||||
data.charts.kandang;
|
|
||||||
seriesData = comparisonChart?.series || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return seriesData
|
|
||||||
.filter((series) => visibleSeries.has(series.id))
|
|
||||||
.map((series, index) => {
|
|
||||||
const isStandard = series.id
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.includes('std');
|
|
||||||
// Use series.id directly as dataKey to match dataset fields
|
|
||||||
const dataKey = series.id.toString();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
key={series.id}
|
|
||||||
type='monotone'
|
|
||||||
dataKey={dataKey}
|
|
||||||
name={series.label}
|
|
||||||
stroke={getLineColor(series.id, index, analysisMode)}
|
|
||||||
opacity={isStandard ? 0.5 : 1}
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeDasharray={isStandard ? '5 5' : undefined}
|
|
||||||
dot={
|
|
||||||
isStandard
|
|
||||||
? false
|
|
||||||
: {
|
|
||||||
r: 3,
|
|
||||||
fill: '#fff',
|
|
||||||
stroke: getLineColor(
|
|
||||||
series.id,
|
|
||||||
index,
|
|
||||||
analysisMode
|
|
||||||
),
|
|
||||||
strokeWidth: 2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
activeDot={isStandard ? undefined : { r: 5 }}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 100];
|
||||||
|
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
|
||||||
|
// Add padding (10% on each side)
|
||||||
|
const padding = (maxValue - minValue) * 0.1;
|
||||||
|
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
||||||
|
const domainMax = Math.ceil(maxValue + padding);
|
||||||
|
|
||||||
|
return [domainMin, domainMax];
|
||||||
|
})()}
|
||||||
|
ticks={(() => {
|
||||||
|
// Calculate dynamic ticks based on domain
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
analysisMode === 'OVERVIEW' &&
|
||||||
|
isOverviewCharts(data.charts)
|
||||||
|
) {
|
||||||
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.location ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
seriesData = comparisonChart?.series || [];
|
||||||
|
dataset = comparisonChart?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleSeriesIds = Array.from(visibleSeries);
|
||||||
|
const allValues: number[] = [];
|
||||||
|
|
||||||
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
|
visibleSeriesIds.forEach((seriesId) => {
|
||||||
|
const value = item[seriesId];
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
allValues.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 25, 50, 75, 100];
|
||||||
|
|
||||||
|
const minValue = Math.min(...allValues);
|
||||||
|
const maxValue = Math.max(...allValues);
|
||||||
|
const padding = (maxValue - minValue) * 0.1;
|
||||||
|
const domainMin = Math.floor(Math.max(0, minValue - padding));
|
||||||
|
const domainMax = Math.ceil(maxValue + padding);
|
||||||
|
|
||||||
|
// Generate 5 evenly spaced ticks
|
||||||
|
const range = domainMax - domainMin;
|
||||||
|
const step = range / 4;
|
||||||
|
|
||||||
|
return [
|
||||||
|
domainMin,
|
||||||
|
Math.round(domainMin + step),
|
||||||
|
Math.round(domainMin + step * 2),
|
||||||
|
Math.round(domainMin + step * 3),
|
||||||
|
domainMax,
|
||||||
|
];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: '#1f2937',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: 'white', marginBottom: '4px' }}
|
||||||
|
itemStyle={{ color: 'white', fontSize: '12px' }}
|
||||||
|
labelFormatter={(value) => `Week ${value}`}
|
||||||
|
formatter={(
|
||||||
|
value: number | undefined,
|
||||||
|
name: string | undefined
|
||||||
|
) => {
|
||||||
|
if (value === undefined || name === undefined) return ['', ''];
|
||||||
|
|
||||||
|
// Get series data to find the unit
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
if (
|
||||||
|
analysisMode === 'OVERVIEW' &&
|
||||||
|
isOverviewCharts(data.charts)
|
||||||
|
) {
|
||||||
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.location ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
seriesData = comparisonChart?.series || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the series that matches this line's name
|
||||||
|
const series = seriesData.find((s) => s.label === name);
|
||||||
|
const unit = series?.unit || '';
|
||||||
|
|
||||||
|
return [`${value} ${unit}`, name];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Dynamic Line rendering based on visible series */}
|
||||||
|
{(() => {
|
||||||
|
let seriesData: DashboardChartsSeries[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
analysisMode === 'OVERVIEW' &&
|
||||||
|
isOverviewCharts(data.charts)
|
||||||
|
) {
|
||||||
|
seriesData = data.charts[chartData]?.series || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.location ||
|
||||||
|
data.charts.flock ||
|
||||||
|
data.charts.kandang;
|
||||||
|
seriesData = comparisonChart?.series || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesData
|
||||||
|
.filter((series) => visibleSeries.has(series.id))
|
||||||
|
.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
// Use series.id directly as dataKey to match dataset fields
|
||||||
|
const dataKey = series.id.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={series.id}
|
||||||
|
type='monotone'
|
||||||
|
dataKey={dataKey}
|
||||||
|
name={series.label}
|
||||||
|
stroke={getLineColor(series.id, index, analysisMode)}
|
||||||
|
opacity={isStandard ? 0.5 : 1}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray={isStandard ? '5 5' : undefined}
|
||||||
|
dot={
|
||||||
|
isStandard
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
r: 3,
|
||||||
|
fill: '#fff',
|
||||||
|
stroke: getLineColor(
|
||||||
|
series.id,
|
||||||
|
index,
|
||||||
|
analysisMode
|
||||||
|
),
|
||||||
|
strokeWidth: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeDot={isStandard ? undefined : { r: 5 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
|
||||||
|
{/* Empty State Overlay */}
|
||||||
|
{(() => {
|
||||||
|
// Get current dataset
|
||||||
|
let dataset: DashboardChartsDataset[] = [];
|
||||||
|
|
||||||
|
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||||
|
dataset = data.charts[chartData]?.dataset || [];
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
const comparisonChart =
|
||||||
|
data.charts.location || data.charts.flock || data.charts.kandang;
|
||||||
|
dataset = comparisonChart?.dataset || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show empty state if dataset is empty
|
||||||
|
if (dataset.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className='absolute inset-x-0 inset-y-15 z-10 flex flex-col items-center justify-center rounded-lg'>
|
||||||
|
{/* Chart icon */}
|
||||||
|
<div className='w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center mb-4'>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:chart-bar'
|
||||||
|
className='text-white'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
/>
|
/>
|
||||||
);
|
</div>
|
||||||
});
|
|
||||||
})()}
|
{/* Empty state text */}
|
||||||
</LineChart>
|
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||||
</ResponsiveContainer>
|
Data Not Yet Available
|
||||||
|
</h3>
|
||||||
|
<p className='text-gray-500 text-sm text-center max-w-xs'>
|
||||||
|
Please change your filters to get the data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError } from '@/lib/api-helper';
|
||||||
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
import { InventoryAdjustmentApi } from '@/services/api/inventory';
|
||||||
import {
|
import {
|
||||||
CreateInventoryAdjustmentPayload,
|
CreateInventoryAdjustmentPayload,
|
||||||
@@ -22,12 +22,18 @@ import {
|
|||||||
} from '@/services/api/master-data';
|
} from '@/services/api/master-data';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, {
|
||||||
|
OptionType,
|
||||||
|
useSelect,
|
||||||
|
} from '@/components/input/SelectInput';
|
||||||
import TextInput from '@/components/input/TextInput';
|
import TextInput from '@/components/input/TextInput';
|
||||||
import { RadioGroup } from '@/components/input/RadioInput';
|
import { RadioGroup } from '@/components/input/RadioInput';
|
||||||
import TextArea from '@/components/input/TextArea';
|
import TextArea from '@/components/input/TextArea';
|
||||||
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
import { useFormikErrorList } from '@/services/hooks/useFormikErrorList';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
|
import { Product } from '@/types/api/master-data/product';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
interface InventoryAdjustmentFormProps {
|
interface InventoryAdjustmentFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -44,10 +50,7 @@ const InventoryAdjustmentForm = ({
|
|||||||
InventoryAdjustmentFormErrorMessage,
|
InventoryAdjustmentFormErrorMessage,
|
||||||
setInventoryAdjustmentFormErrorMessage,
|
setInventoryAdjustmentFormErrorMessage,
|
||||||
] = useState('');
|
] = useState('');
|
||||||
const [selectedProductCategories, setSelectedProductCategories] =
|
|
||||||
useState('');
|
|
||||||
const [disabledProduct, setDisabledProduct] = useState(true);
|
const [disabledProduct, setDisabledProduct] = useState(true);
|
||||||
const [optionsProduct, setOptionsProduct] = useState<OptionType[]>([]);
|
|
||||||
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
|
const [quantityLabel, setQuantityLabel] = useState('Tambah Stok');
|
||||||
|
|
||||||
// Submit Handler
|
// Submit Handler
|
||||||
@@ -108,45 +111,30 @@ const InventoryAdjustmentForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const productCategoriesUrl = `${
|
const {
|
||||||
ProductCategoryApi.basePath
|
setInputValue: setProductCategoryInputValue,
|
||||||
}?${new URLSearchParams({
|
options: productCategoryOptions,
|
||||||
search: '',
|
isLoadingOptions: isLoadingProductCategoryOptions,
|
||||||
}).toString()}`;
|
loadMore: loadMoreProductCategories,
|
||||||
const { data: productCategories, isLoading: isLoadingProductCategories } =
|
} = useSelect<ProductCategory>(ProductCategoryApi.basePath, 'id', 'name');
|
||||||
useSWR(productCategoriesUrl, ProductCategoryApi.getAllFetcher);
|
|
||||||
|
|
||||||
const productUrl = `${ProductApi.basePath}?${new URLSearchParams({
|
const {
|
||||||
search: '',
|
setInputValue: setProductInputValue,
|
||||||
product_category_id: selectedProductCategories,
|
options: productOptions,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingProductOptions,
|
||||||
const { data: products, isLoading: isLoadingProducts } = useSWR(
|
loadMore: loadMoreProducts,
|
||||||
productUrl,
|
} = useSelect<Product>(ProductApi.basePath, 'id', 'name', 'search', {
|
||||||
ProductApi.getAllFetcher
|
product_category_id: formik.values.product_category_id
|
||||||
);
|
? String(formik.values.product_category_id)
|
||||||
|
: '',
|
||||||
|
});
|
||||||
|
|
||||||
const warehouseUrl = `${WarehouseApi.basePath}?${new URLSearchParams({
|
const {
|
||||||
search: '',
|
setInputValue: setWarehouseInputValue,
|
||||||
limit: '100',
|
options: warehouseOptions,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
const { data: warehouses, isLoading: isLoadingWarehouses } = useSWR(
|
loadMore: loadMoreWarehouses,
|
||||||
warehouseUrl,
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// Map Data to Options
|
|
||||||
const optionsProductCategory = isResponseSuccess(productCategories)
|
|
||||||
? productCategories?.data.map((productCategory) => ({
|
|
||||||
value: productCategory.id,
|
|
||||||
label: productCategory.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsWarehouse = isResponseSuccess(warehouses)
|
|
||||||
? warehouses?.data.map((warehouse) => ({
|
|
||||||
value: warehouse.id,
|
|
||||||
label: warehouse.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Options Handler
|
// Options Handler
|
||||||
const productCategoryChangeHandler = (
|
const productCategoryChangeHandler = (
|
||||||
@@ -157,7 +145,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
|
|
||||||
formik.setFieldValue('product_category', val);
|
formik.setFieldValue('product_category', val);
|
||||||
|
|
||||||
setSelectedProductCategories((val as OptionType)?.value as string);
|
|
||||||
const disabled = (val as OptionType)?.value == null;
|
const disabled = (val as OptionType)?.value == null;
|
||||||
setDisabledProduct(disabled);
|
setDisabledProduct(disabled);
|
||||||
formik.setFieldValue('product_id', 0);
|
formik.setFieldValue('product_id', 0);
|
||||||
@@ -193,9 +180,6 @@ const InventoryAdjustmentForm = ({
|
|||||||
// Effect
|
// Effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialValues?.product_warehouse?.product?.id) {
|
if (initialValues?.product_warehouse?.product?.id) {
|
||||||
setSelectedProductCategories(
|
|
||||||
String(initialValues.product_warehouse.product.id)
|
|
||||||
);
|
|
||||||
setDisabledProduct(false);
|
setDisabledProduct(false);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
'product_id',
|
'product_id',
|
||||||
@@ -219,25 +203,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
);
|
);
|
||||||
formik.setFieldValue('note', initialValues.note);
|
formik.setFieldValue('note', initialValues.note);
|
||||||
}
|
}
|
||||||
}, [
|
}, [formik, initialValues, setQuantityLabel, setDisabledProduct]);
|
||||||
formik,
|
|
||||||
initialValues,
|
|
||||||
setQuantityLabel,
|
|
||||||
setDisabledProduct,
|
|
||||||
setSelectedProductCategories,
|
|
||||||
]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
formikSetValues(formikInitialValues as InventoryAdjustmentFormValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
useEffect(() => {
|
|
||||||
if (isResponseSuccess(products)) {
|
|
||||||
const options = products.data.map((p) => ({
|
|
||||||
value: p.id,
|
|
||||||
label: p.name,
|
|
||||||
}));
|
|
||||||
setOptionsProduct(options);
|
|
||||||
}
|
|
||||||
}, [products]);
|
|
||||||
|
|
||||||
// Utils Function
|
// Utils Function
|
||||||
const formatNumber = (value: string) => {
|
const formatNumber = (value: string) => {
|
||||||
@@ -282,9 +251,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Kategori Produk'
|
label='Kategori Produk'
|
||||||
value={formik.values.product_category as OptionType}
|
value={formik.values.product_category as OptionType}
|
||||||
onChange={productCategoryChangeHandler}
|
onChange={productCategoryChangeHandler}
|
||||||
onInputChange={setSelectedProductCategories}
|
onInputChange={setProductCategoryInputValue}
|
||||||
options={optionsProductCategory}
|
options={productCategoryOptions}
|
||||||
isLoading={isLoadingProductCategories}
|
onMenuScrollToBottom={loadMoreProductCategories}
|
||||||
|
isLoading={isLoadingProductCategoryOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.product_category &&
|
formik.touched.product_category &&
|
||||||
Boolean(formik.errors.product_category)
|
Boolean(formik.errors.product_category)
|
||||||
@@ -300,8 +270,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Produk'
|
label='Produk'
|
||||||
value={formik.values.product as OptionType}
|
value={formik.values.product as OptionType}
|
||||||
onChange={productChangeHandler}
|
onChange={productChangeHandler}
|
||||||
options={optionsProduct}
|
onInputChange={setProductInputValue}
|
||||||
isLoading={isLoadingProducts}
|
options={productOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreProducts}
|
||||||
|
isLoading={isLoadingProductOptions}
|
||||||
isError={formik.touched.product && Boolean(formik.errors.product)}
|
isError={formik.touched.product && Boolean(formik.errors.product)}
|
||||||
errorMessage={formik.errors.product as string}
|
errorMessage={formik.errors.product as string}
|
||||||
isDisabled={type === 'detail' || disabledProduct}
|
isDisabled={type === 'detail' || disabledProduct}
|
||||||
@@ -314,8 +286,10 @@ const InventoryAdjustmentForm = ({
|
|||||||
label='Warehouse'
|
label='Warehouse'
|
||||||
value={formik.values.warehouse as OptionType}
|
value={formik.values.warehouse as OptionType}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
options={optionsWarehouse}
|
onInputChange={setWarehouseInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
options={warehouseOptions}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
|
isLoading={isLoadingWarehouseOptions}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.warehouse && Boolean(formik.errors.warehouse)
|
formik.touched.warehouse && Boolean(formik.errors.warehouse)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import Card from '@/components/Card';
|
|||||||
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
|
||||||
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
import { getUniqueFormikErrors } from '@/lib/formik-helper';
|
||||||
import AlertErrorList from '@/components/helper/form/FormErrors';
|
import AlertErrorList from '@/components/helper/form/FormErrors';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
|
|
||||||
interface MovementFormProps {
|
interface MovementFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -49,10 +51,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
const [movementFormErrorMessage, setMovementFormErrorMessage] = useState('');
|
||||||
const [
|
|
||||||
productWarehouseSelectInputValue,
|
|
||||||
setProductWarehouseSelectInputValue,
|
|
||||||
] = useState('');
|
|
||||||
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
const [selectedProducts, setSelectedProducts] = useState<number[]>([]);
|
||||||
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
const [selectedDeliveries, setSelectedDeliveries] = useState<number[]>([]);
|
||||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||||
@@ -93,10 +91,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
|
|
||||||
// ===== USE SELECT HOOKS =====
|
// ===== USE SELECT HOOKS =====
|
||||||
const {
|
const {
|
||||||
inputValue: warehouseSelectInputValue,
|
|
||||||
setInputValue: setWarehouseSelectInputValue,
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
loadMore: loadMoreWarehouses,
|
||||||
|
rawData: warehouses,
|
||||||
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== SELECT INPUT DATA =====
|
// ===== SELECT INPUT DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -107,12 +106,6 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
category: 'BOP',
|
category: 'BOP',
|
||||||
});
|
});
|
||||||
|
|
||||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
|
||||||
const { data: warehouses } = useSWR(
|
|
||||||
warehousesUrl,
|
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== DATA PROCESSING =====
|
// ===== DATA PROCESSING =====
|
||||||
const warehouseStockMap = useMemo(() => {
|
const warehouseStockMap = useMemo(() => {
|
||||||
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
if (!isResponseSuccess(allProductWarehouses)) return new Map();
|
||||||
@@ -269,25 +262,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
const getProductWarehousesUrl = useCallback(() => {
|
const {
|
||||||
const productWarehouseParams = new URLSearchParams({
|
setInputValue: setProductWarehouseSelectInputValue,
|
||||||
search: productWarehouseSelectInputValue,
|
isLoadingOptions: isLoadingProductWarehouses,
|
||||||
});
|
loadMore: loadMoreProductWarehouses,
|
||||||
if (formik.values.source_warehouse_id) {
|
rawData: productWarehouses,
|
||||||
productWarehouseParams.append(
|
} = useSelect<ProductWarehouse>(
|
||||||
'warehouse_id',
|
formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null,
|
||||||
formik.values.source_warehouse_id.toString()
|
'id',
|
||||||
);
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
|
? formik.values.source_warehouse_id.toString()
|
||||||
|
: '',
|
||||||
}
|
}
|
||||||
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
);
|
||||||
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
|
|
||||||
|
|
||||||
const productWarehousesUrl = getProductWarehousesUrl();
|
|
||||||
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
|
||||||
useSWR(
|
|
||||||
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
|
||||||
ProductWarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
? productWarehouses?.data.map((pw) => ({
|
? productWarehouses?.data.map((pw) => ({
|
||||||
@@ -1006,6 +996,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}}
|
}}
|
||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.source_warehouse_id &&
|
formik.touched.source_warehouse_id &&
|
||||||
@@ -1104,6 +1095,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.destination_warehouse_id &&
|
formik.touched.destination_warehouse_id &&
|
||||||
Boolean(formik.errors.destination_warehouse_id)
|
Boolean(formik.errors.destination_warehouse_id)
|
||||||
@@ -1263,6 +1255,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}}
|
}}
|
||||||
options={productWarehouseOptions}
|
options={productWarehouseOptions}
|
||||||
onInputChange={setProductWarehouseSelectInputValue}
|
onInputChange={setProductWarehouseSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||||
isLoading={isLoadingProductWarehouses}
|
isLoading={isLoadingProductWarehouses}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
type === 'detail' ||
|
type === 'detail' ||
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -233,7 +215,7 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama kandang'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -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);
|
||||||
@@ -248,7 +229,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama nonstock'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as Yup from 'yup';
|
|||||||
type ProductFormSchemaType = {
|
type ProductFormSchemaType = {
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
uom?: {
|
uom?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -15,10 +15,16 @@ type ProductFormSchemaType = {
|
|||||||
} | null;
|
} | null;
|
||||||
product_category_id: number;
|
product_category_id: number;
|
||||||
product_price: number | string;
|
product_price: number | string;
|
||||||
selling_price: number | string;
|
selling_price?: number | string;
|
||||||
tax: number | string;
|
tax?: number | string;
|
||||||
expiry_period: number | string;
|
expiry_period?: number | string;
|
||||||
supplier_ids: number[];
|
suppliers: {
|
||||||
|
supplier: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +32,7 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
Yup.object({
|
Yup.object({
|
||||||
name: Yup.string().required('Nama wajib diisi!'),
|
name: Yup.string().required('Nama wajib diisi!'),
|
||||||
brand: Yup.string().required('Merek wajib diisi!'),
|
brand: Yup.string().required('Merek wajib diisi!'),
|
||||||
sku: Yup.string().required('SKU wajib diisi!'),
|
sku: Yup.string(),
|
||||||
|
|
||||||
uom: Yup.object({
|
uom: Yup.object({
|
||||||
value: Yup.number()
|
value: Yup.number()
|
||||||
@@ -58,24 +64,34 @@ export const ProductFormSchema: Yup.ObjectSchema<ProductFormSchemaType> =
|
|||||||
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
.min(1, 'Harga produk tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
selling_price: Yup.number()
|
selling_price: Yup.number()
|
||||||
.required('Harga jual wajib diisi!')
|
.typeError('Harga hanya boleh angka!')
|
||||||
.typeError('Harga jual wajib diisi!')
|
|
||||||
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
.min(1, 'Harga jual tidak boleh kurang dari 1!'),
|
||||||
|
|
||||||
tax: Yup.number()
|
tax: Yup.number()
|
||||||
.required('Pajak wajib diisi!')
|
.typeError('Pajak hanya boleh angka!')
|
||||||
.typeError('Pajak wajib diisi!')
|
|
||||||
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
.min(0, 'Pajak tidak boleh kurang dari 0!')
|
||||||
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
.max(100, 'Pajak tidak boleh lebih dari 100%!'),
|
||||||
|
|
||||||
expiry_period: Yup.number()
|
expiry_period: Yup.number()
|
||||||
.required('Periode kadaluarsa wajib diisi!')
|
.typeError('Periode kadaluarsa hanya boleh angka!')
|
||||||
.typeError('Periode kadaluarsa wajib diisi!')
|
|
||||||
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
.min(1, 'Periode kadaluarsa tidak boleh kurang dari 1 hari!'),
|
||||||
|
|
||||||
supplier_ids: Yup.array()
|
suppliers: Yup.array()
|
||||||
.of(Yup.number().required().typeError('Supplier tidak valid!'))
|
.of(
|
||||||
.min(1, 'Minimal harus ada 1 supplier!')
|
Yup.object({
|
||||||
|
supplier: Yup.object({
|
||||||
|
value: Yup.number()
|
||||||
|
.min(1, 'Supplier wajib dipilih!')
|
||||||
|
.required('Supplier wajib dipilih!')
|
||||||
|
.typeError('Supplier wajib dipilih!'),
|
||||||
|
label: Yup.string().required('Supplier wajib dipilih!'),
|
||||||
|
}).required('Supplier wajib dipilih!'),
|
||||||
|
price: Yup.number()
|
||||||
|
.min(1, 'Harga tidak boleh kurang dari 1!')
|
||||||
|
.required('Harga wajib diisi!')
|
||||||
|
.typeError('Harga wajib diisi!'),
|
||||||
|
})
|
||||||
|
)
|
||||||
.required('Supplier wajib diisi!'),
|
.required('Supplier wajib diisi!'),
|
||||||
|
|
||||||
flags: Yup.array()
|
flags: Yup.array()
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ 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';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import { removeArrayItemAndSync } from '@/lib/utils/formik';
|
||||||
|
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
type?: 'add' | 'edit' | 'detail';
|
type?: 'add' | 'edit' | 'detail';
|
||||||
@@ -100,7 +103,15 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
selling_price: initialValues?.selling_price ?? '',
|
selling_price: initialValues?.selling_price ?? '',
|
||||||
tax: initialValues?.tax ?? '',
|
tax: initialValues?.tax ?? '',
|
||||||
expiry_period: initialValues?.expiry_period ?? '',
|
expiry_period: initialValues?.expiry_period ?? '',
|
||||||
supplier_ids: initialValues?.suppliers?.map((s) => s.id) ?? [],
|
suppliers: initialValues?.suppliers
|
||||||
|
? initialValues.suppliers.map((supplier) => ({
|
||||||
|
supplier: {
|
||||||
|
value: supplier.id,
|
||||||
|
label: supplier.name,
|
||||||
|
},
|
||||||
|
price: supplier.price,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
flags: initialValues?.flags ?? [],
|
flags: initialValues?.flags ?? [],
|
||||||
}),
|
}),
|
||||||
[initialValues]
|
[initialValues]
|
||||||
@@ -119,12 +130,17 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
uom_id: values.uom_id,
|
uom_id: values.uom_id,
|
||||||
product_category_id: values.product_category_id,
|
product_category_id: values.product_category_id,
|
||||||
product_price: parseInt(values.product_price.toString()) || 0,
|
product_price: parseInt(values.product_price.toString()) || 0,
|
||||||
selling_price: parseInt(values.selling_price.toString()) || 0,
|
selling_price: values.selling_price
|
||||||
tax: parseInt(values.tax.toString()) || 0,
|
? parseInt(values.selling_price.toString()) || 0
|
||||||
expiry_period: parseInt(values.expiry_period.toString()) || 0,
|
: undefined,
|
||||||
supplier_ids: values.supplier_ids.filter(
|
tax: values.tax ? parseInt(values.tax.toString()) || 0 : undefined,
|
||||||
(id): id is number => typeof id === 'number'
|
expiry_period: values.expiry_period
|
||||||
),
|
? parseInt(values.expiry_period.toString()) || 0
|
||||||
|
: undefined,
|
||||||
|
suppliers: values.suppliers.map((s) => ({
|
||||||
|
supplier_id: s.supplier?.value as number,
|
||||||
|
price: parseInt(s.price.toString()) || 0,
|
||||||
|
})),
|
||||||
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
flags: values.flags.filter((f): f is string => typeof f === 'string'),
|
||||||
};
|
};
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -145,6 +161,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 +175,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,24 +185,38 @@ 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 filteredSupplierOptions = useMemo(() => {
|
||||||
: [];
|
return supplierOptions.filter((opt) => {
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
return !formik.values.suppliers.some(
|
||||||
const arr = Array.isArray(val) ? val : val ? [val] : [];
|
(s) => s.supplier?.value === opt.value
|
||||||
formik.setFieldTouched('supplier_ids', true);
|
);
|
||||||
formik.setFieldValue(
|
});
|
||||||
'supplier_ids',
|
}, [supplierOptions, formik.values.suppliers]);
|
||||||
arr.map((v) => (v as OptionType).value)
|
|
||||||
);
|
const addSupplierHandler = () => {
|
||||||
|
formik.setFieldValue('suppliers', [
|
||||||
|
...formik.values.suppliers,
|
||||||
|
{
|
||||||
|
supplier_id: '',
|
||||||
|
price: formik.values.product_price,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSupplierItemHandler = (idx: number) => {
|
||||||
|
const path = 'suppliers';
|
||||||
|
|
||||||
|
// trims values, errors, and touched at idx
|
||||||
|
removeArrayItemAndSync(formik, path, idx);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteProductClickHandler = () => {
|
const deleteProductClickHandler = () => {
|
||||||
@@ -200,6 +232,19 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
router.push('/master-data/product');
|
router.push('/master-data/product');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSupplierRepeaterError = (
|
||||||
|
column: 'supplier' | 'price',
|
||||||
|
supplierIdx: number
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
formik.touched.suppliers?.[supplierIdx]?.[column] &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors.suppliers?.[supplierIdx] instanceof Object &&
|
||||||
|
formik.errors.suppliers?.[supplierIdx]?.[column]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formikSetValues(formikInitialValues);
|
formikSetValues(formikInitialValues);
|
||||||
}, [formikSetValues, formikInitialValues]);
|
}, [formikSetValues, formikInitialValues]);
|
||||||
@@ -270,7 +315,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
|
||||||
label='SKU'
|
label='SKU'
|
||||||
name='sku'
|
name='sku'
|
||||||
placeholder='Masukkan SKU...'
|
placeholder='Masukkan SKU...'
|
||||||
@@ -291,6 +335,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 +353,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 ||
|
||||||
@@ -341,7 +387,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Harga Jual'
|
label='Harga Jual'
|
||||||
name='selling_price'
|
name='selling_price'
|
||||||
placeholder='Masukkan harga jual...'
|
placeholder='Masukkan harga jual...'
|
||||||
@@ -363,7 +408,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-2 gap-4'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Pajak (%)'
|
label='Pajak (%)'
|
||||||
name='tax'
|
name='tax'
|
||||||
placeholder='Masukkan pajak...'
|
placeholder='Masukkan pajak...'
|
||||||
@@ -380,7 +424,6 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
label='Periode Kadaluarsa (hari)'
|
label='Periode Kadaluarsa (hari)'
|
||||||
name='expiry_period'
|
name='expiry_period'
|
||||||
placeholder='Masukkan periode kadaluarsa...'
|
placeholder='Masukkan periode kadaluarsa...'
|
||||||
@@ -400,27 +443,7 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
readOnly={type === 'detail'}
|
readOnly={type === 'detail'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
<SelectInput
|
|
||||||
required
|
|
||||||
label='Supplier'
|
|
||||||
placeholder='Pilih supplier...'
|
|
||||||
isMulti
|
|
||||||
value={supplierOptions.filter((opt) =>
|
|
||||||
(formik.values.supplier_ids || []).includes(opt.value)
|
|
||||||
)}
|
|
||||||
onChange={supplierChangeHandler}
|
|
||||||
options={supplierOptions}
|
|
||||||
onInputChange={setSupplierSelectInputValue}
|
|
||||||
isLoading={isLoadingSuppliers}
|
|
||||||
isError={
|
|
||||||
formik.touched.supplier_ids &&
|
|
||||||
Boolean(formik.errors.supplier_ids)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.supplier_ids as string}
|
|
||||||
isDisabled={type === 'detail'}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
label='Flags'
|
label='Flags'
|
||||||
@@ -443,6 +466,129 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
|
|||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='grid sm:grid-cols-1 gap-4'>
|
||||||
|
{type !== 'detail' && formik.values.suppliers.length === 0 && (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
className='w-fit mx-auto'
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} /> Tambah
|
||||||
|
Supplier
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{formik.values.suppliers.length > 0 && (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='mb-4 text-center'>
|
||||||
|
<h4 className='font-bold text-xl'>Supplier</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='overflow-x-auto'>
|
||||||
|
<table className='table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Supplier
|
||||||
|
</th>
|
||||||
|
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
|
||||||
|
Harga
|
||||||
|
</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{formik.values.suppliers.map((supplier, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<SelectInput
|
||||||
|
placeholder='Pilih Supplier'
|
||||||
|
options={filteredSupplierOptions}
|
||||||
|
onInputChange={setSupplierSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSuppliers}
|
||||||
|
value={formik.values.suppliers[idx].supplier}
|
||||||
|
onChange={(val) => {
|
||||||
|
formik.setFieldValue(
|
||||||
|
`suppliers.${idx}.supplier`,
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
isError={isSupplierRepeaterError(
|
||||||
|
'supplier',
|
||||||
|
idx
|
||||||
|
)}
|
||||||
|
isClearable
|
||||||
|
isDisabled={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className='p-2 w-full max-w-1/2'>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
name={`suppliers.${idx}.price`}
|
||||||
|
placeholder='Masukkan harga...'
|
||||||
|
value={formik.values.suppliers[idx].price}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
decimalScale={2}
|
||||||
|
allowNegative={false}
|
||||||
|
thousandSeparator=','
|
||||||
|
decimalSeparator='.'
|
||||||
|
inputPrefix='Rp '
|
||||||
|
isError={isSupplierRepeaterError('price', idx)}
|
||||||
|
readOnly={type === 'detail'}
|
||||||
|
className={{
|
||||||
|
wrapper: 'min-w-48 w-full',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='error'
|
||||||
|
onClick={() => deleteSupplierItemHandler(idx)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:delete-outline-rounded'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type !== 'detail' && (
|
||||||
|
<div className='w-full flex flex-row justify-center'>
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
color='success'
|
||||||
|
onClick={addSupplierHandler}
|
||||||
|
>
|
||||||
|
<Icon icon='ic:round-plus' width={24} height={24} />{' '}
|
||||||
|
Tambah Supplier
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||||
{type !== 'add' && (
|
{type !== 'add' && (
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -358,7 +330,7 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
|
|||||||
required
|
required
|
||||||
label='Nama'
|
label='Nama'
|
||||||
name='name'
|
name='name'
|
||||||
placeholder='Masukkan nama lokasi'
|
placeholder='Masukkan nama warehouse'
|
||||||
value={formik.values.name}
|
value={formik.values.name}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -678,12 +678,13 @@ const RecordingTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Nama Project',
|
header: 'Nama Project',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
`Project ${props.row.original.project_flock_kandang_id}`,
|
props.row.original.project_flock?.flock_name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Kategori',
|
header: 'Kategori',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const category = props.row.original.project_flock_category;
|
const category =
|
||||||
|
props.row.original.project_flock?.project_flock_category;
|
||||||
if (!category) return '-';
|
if (!category) return '-';
|
||||||
const color = category === 'LAYING' ? 'info' : 'warning';
|
const color = category === 'LAYING' ? 'info' : 'warning';
|
||||||
return (
|
return (
|
||||||
@@ -706,7 +707,8 @@ const RecordingTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Populasi Awal',
|
header: 'Populasi Awal',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
props.row.original.total_chick_qty?.toLocaleString() || '-',
|
props.row.original.project_flock?.total_chick_qty?.toLocaleString() ||
|
||||||
|
'-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
|
|||||||
@@ -117,8 +117,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
// ===== PAYLOAD CREATION HELPERS =====
|
// ===== PAYLOAD CREATION HELPERS =====
|
||||||
const createGrowingPayload = useCallback(
|
const createGrowingPayload = useCallback(
|
||||||
(values: RecordingGrowingFormValues) => {
|
(values: RecordingGrowingFormValues) => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
return {
|
return {
|
||||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||||
|
record_date: today,
|
||||||
stocks: (values.stocks ?? []).map((stock) => ({
|
stocks: (values.stocks ?? []).map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
@@ -134,8 +136,10 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
|
|
||||||
const createLayingPayload = useCallback(
|
const createLayingPayload = useCallback(
|
||||||
(values: RecordingLayingFormValues) => {
|
(values: RecordingLayingFormValues) => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
return {
|
return {
|
||||||
project_flock_kandang_id: values.project_flock_kandang_id,
|
project_flock_kandang_id: values.project_flock_kandang_id,
|
||||||
|
record_date: today,
|
||||||
stocks: (values.stocks ?? []).map((stock) => ({
|
stocks: (values.stocks ?? []).map((stock) => ({
|
||||||
product_warehouse_id: stock.product_warehouse_id,
|
product_warehouse_id: stock.product_warehouse_id,
|
||||||
qty: Number(stock.qty) || 0,
|
qty: Number(stock.qty) || 0,
|
||||||
@@ -252,9 +256,13 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const projectFlockKandangDetailUrl = useMemo(() => {
|
const projectFlockKandangDetailUrl = useMemo(() => {
|
||||||
if (type === 'add' || !initialValues?.project_flock_kandang_id) return null;
|
if (
|
||||||
return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock_kandang_id}`;
|
type === 'add' ||
|
||||||
}, [type, initialValues?.project_flock_kandang_id]);
|
!initialValues?.project_flock?.project_flock_kandang_id
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
return `${ProjectFlockKandangApi.basePath}/${initialValues.project_flock.project_flock_kandang_id}`;
|
||||||
|
}, [type, initialValues?.project_flock?.project_flock_kandang_id]);
|
||||||
|
|
||||||
const { data: projectFlockKandangDetailData } = useSWR(
|
const { data: projectFlockKandangDetailData } = useSWR(
|
||||||
projectFlockKandangDetailUrl,
|
projectFlockKandangDetailUrl,
|
||||||
@@ -404,12 +412,12 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
}, [approvedProjectFlockKandangsData]);
|
}, [approvedProjectFlockKandangsData]);
|
||||||
|
|
||||||
const isLayingCategory =
|
const isLayingCategory =
|
||||||
initialValues?.project_flock_category === 'LAYING' ||
|
initialValues?.project_flock?.project_flock_category === 'LAYING' ||
|
||||||
projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
|
projectFlockKandangLookup?.project_flock?.category === 'LAYING' ||
|
||||||
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
|
projectFlockKandangDetail?.project_flock?.category === 'LAYING';
|
||||||
|
|
||||||
const isGrowingCategory =
|
const isGrowingCategory =
|
||||||
initialValues?.project_flock_category === 'GROWING' ||
|
initialValues?.project_flock?.project_flock_category === 'GROWING' ||
|
||||||
projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
|
projectFlockKandangLookup?.project_flock?.category === 'GROWING' ||
|
||||||
projectFlockKandangDetail?.project_flock?.category === 'GROWING';
|
projectFlockKandangDetail?.project_flock?.category === 'GROWING';
|
||||||
|
|
||||||
@@ -555,7 +563,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
todayRecordings.forEach((recording) => {
|
todayRecordings.forEach((recording) => {
|
||||||
const recordingDate = recording.record_datetime?.split('T')[0];
|
const recordingDate = recording.record_datetime?.split('T')[0];
|
||||||
if (recordingDate === today) {
|
if (recordingDate === today) {
|
||||||
recordedIds.add(recording.project_flock_kandang_id);
|
recordedIds.add(recording.project_flock.project_flock_kandang_id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1005,7 +1013,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
const hasSameDayRecording = isResponseSuccess(existingRecordings)
|
const hasSameDayRecording = isResponseSuccess(existingRecordings)
|
||||||
? existingRecordings.data?.some(
|
? existingRecordings.data?.some(
|
||||||
(recording: Recording) =>
|
(recording: Recording) =>
|
||||||
recording.project_flock_kandang_id ===
|
recording.project_flock.project_flock_kandang_id ===
|
||||||
projectFlockKandangId &&
|
projectFlockKandangId &&
|
||||||
recording.day === nextDayRecording.next_day
|
recording.day === nextDayRecording.next_day
|
||||||
)
|
)
|
||||||
@@ -1543,13 +1551,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={
|
color={
|
||||||
initialValues.project_flock_category === 'LAYING'
|
initialValues.project_flock
|
||||||
|
?.project_flock_category === 'LAYING'
|
||||||
? 'info'
|
? 'info'
|
||||||
: 'warning'
|
: 'warning'
|
||||||
}
|
}
|
||||||
size='sm'
|
size='sm'
|
||||||
>
|
>
|
||||||
{initialValues.project_flock_category}
|
{initialValues.project_flock?.project_flock_category}
|
||||||
</Badge>
|
</Badge>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1579,7 +1588,7 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
{type === 'detail' && initialValues && (
|
{type === 'detail' && initialValues && (
|
||||||
<div
|
<div
|
||||||
className={`grid gap-6 mb-6 grid-cols-1 ${
|
className={`grid gap-6 mb-6 grid-cols-1 ${
|
||||||
initialValues.project_flock_category === 'LAYING'
|
initialValues.project_flock?.project_flock_category === 'LAYING'
|
||||||
? 'xl:grid-cols-3'
|
? 'xl:grid-cols-3'
|
||||||
: 'xl:grid-cols-2'
|
: 'xl:grid-cols-2'
|
||||||
}`}
|
}`}
|
||||||
@@ -1614,8 +1623,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.fcr_std && initialValues.fcr_std > 0
|
{initialValues.project_flock?.fcr?.fcr_std &&
|
||||||
? formatNumber(initialValues.fcr_std)
|
initialValues.project_flock?.fcr?.fcr_std > 0
|
||||||
|
? formatNumber(
|
||||||
|
initialValues.project_flock?.fcr?.fcr_std
|
||||||
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1630,9 +1642,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.feed_intake_std &&
|
{initialValues.project_flock?.production_standart
|
||||||
initialValues.feed_intake_std > 0
|
?.feed_intake_std &&
|
||||||
? formatNumber(initialValues.feed_intake_std)
|
initialValues.project_flock?.production_standart
|
||||||
|
?.feed_intake_std > 0
|
||||||
|
? formatNumber(
|
||||||
|
initialValues.project_flock?.production_standart
|
||||||
|
?.feed_intake_std
|
||||||
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1650,59 +1667,39 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='p-4'>
|
<div className='p-4'>
|
||||||
<table className='w-full text-sm'>
|
<table className='w-full text-sm'>
|
||||||
<thead>
|
|
||||||
<tr className='border-b border-gray-200'>
|
|
||||||
<th
|
|
||||||
colSpan={2}
|
|
||||||
className='text-center py-2 font-semibold text-gray-600'
|
|
||||||
>
|
|
||||||
DEPLESI KUMULATIF
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr className='border-b border-gray-200'>
|
|
||||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
|
||||||
Total
|
|
||||||
</th>
|
|
||||||
<th className='text-center py-2 font-semibold text-xs text-gray-500'>
|
|
||||||
(%)
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='text-center py-3 border-r border-gray-100'>
|
<td className='py-2 font-medium'>Deplesi Kumulatif</td>
|
||||||
|
<td className='text-right py-2'>
|
||||||
<span className='font-semibold'>
|
<span className='font-semibold'>
|
||||||
{initialValues.total_depletion_qty &&
|
{initialValues.cum_depletion_rate &&
|
||||||
initialValues.total_depletion_qty > 0
|
initialValues.cum_depletion_rate > 0
|
||||||
? formatNumber(initialValues.total_depletion_qty)
|
? `${initialValues.cum_depletion_rate.toFixed(2)}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
|
||||||
{initialValues.cum_depletion_rate &&
|
|
||||||
initialValues.cum_depletion_rate > 0
|
|
||||||
? initialValues.cum_depletion_rate.toFixed(2)
|
|
||||||
: '-'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td className='py-2 font-medium'>Total Depletion</td>
|
||||||
colSpan={2}
|
<td className='text-right py-2 font-semibold'>
|
||||||
className='text-center py-3 border-r border-gray-200 text-gray-600'
|
{initialValues.total_depletion_qty &&
|
||||||
>
|
initialValues.total_depletion_qty > 0
|
||||||
Total Ayam
|
? formatNumber(initialValues.total_depletion_qty)
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
colSpan={2}
|
|
||||||
className='text-center py-3 font-semibold'
|
|
||||||
>
|
|
||||||
{initialValues.total_chick_qty &&
|
|
||||||
initialValues.total_chick_qty > 0
|
|
||||||
? formatNumber(initialValues.total_chick_qty)
|
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<tr className='border-t border-gray-200'>
|
||||||
|
<td className='py-2 text-gray-600'>Total Ayam</td>
|
||||||
|
<td className='text-right py-2 font-semibold'>
|
||||||
|
{initialValues.project_flock?.total_chick_qty &&
|
||||||
|
initialValues.project_flock?.total_chick_qty > 0
|
||||||
|
? formatNumber(
|
||||||
|
initialValues.project_flock?.total_chick_qty
|
||||||
|
)
|
||||||
|
: '-'}
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -1712,7 +1709,8 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
{/* Egg Production Section - Only for LAYING category */}
|
{/* Egg Production Section - Only for LAYING category */}
|
||||||
{type === 'detail' &&
|
{type === 'detail' &&
|
||||||
initialValues &&
|
initialValues &&
|
||||||
initialValues.project_flock_category === 'LAYING' && (
|
initialValues.project_flock?.project_flock_category ===
|
||||||
|
'LAYING' && (
|
||||||
<div className='border border-gray-200 rounded-lg bg-white'>
|
<div className='border border-gray-200 rounded-lg bg-white'>
|
||||||
<div className='px-4 py-3 border-b border-gray-200'>
|
<div className='px-4 py-3 border-b border-gray-200'>
|
||||||
<span className='card-title font-bold text-xl'>
|
<span className='card-title font-bold text-xl'>
|
||||||
@@ -1744,9 +1742,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.egg_mass_std &&
|
{initialValues.project_flock?.production_standart
|
||||||
initialValues.egg_mass_std > 0
|
?.egg_mass_std &&
|
||||||
? formatNumber(initialValues.egg_mass_std)
|
initialValues.project_flock?.production_standart
|
||||||
|
?.egg_mass_std > 0
|
||||||
|
? formatNumber(
|
||||||
|
initialValues.project_flock
|
||||||
|
?.production_standart?.egg_mass_std
|
||||||
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1763,9 +1766,14 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.egg_weight_std &&
|
{initialValues.project_flock?.production_standart
|
||||||
initialValues.egg_weight_std > 0
|
?.egg_weight_std &&
|
||||||
? formatNumber(initialValues.egg_weight_std)
|
initialValues.project_flock?.production_standart
|
||||||
|
?.egg_weight_std > 0
|
||||||
|
? formatNumber(
|
||||||
|
initialValues.project_flock
|
||||||
|
?.production_standart?.egg_weight_std
|
||||||
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1780,9 +1788,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.hen_day_std !== undefined &&
|
{initialValues.project_flock?.production_standart
|
||||||
initialValues.hen_day_std > 0
|
?.hen_day_std !== undefined &&
|
||||||
? `${initialValues.hen_day_std}%`
|
initialValues.project_flock?.production_standart
|
||||||
|
?.hen_day_std > 0
|
||||||
|
? `${initialValues.project_flock?.production_standart?.hen_day_std}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -1797,9 +1807,11 @@ const RecordingForm = ({ type = 'add', initialValues }: RecordingFormProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='text-center py-3 text-gray-600'>
|
<td className='text-center py-3 text-gray-600'>
|
||||||
{initialValues.hen_house_std !== undefined &&
|
{initialValues.project_flock?.production_standart
|
||||||
initialValues.hen_house_std > 0
|
?.hen_house_std !== undefined &&
|
||||||
? `${initialValues.hen_house_std}%`
|
initialValues.project_flock?.production_standart
|
||||||
|
?.hen_house_std > 0
|
||||||
|
? `${initialValues.project_flock?.production_standart?.hen_house_std}%`
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
import { pdf } from '@react-pdf/renderer';
|
import { pdf } from '@react-pdf/renderer';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -28,7 +28,10 @@ import {
|
|||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { Customer } from '@/types/api/master-data/customer';
|
import { Customer } from '@/types/api/master-data/customer';
|
||||||
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
import { MarketingReportApi } from '@/services/api/report/marketing-report';
|
||||||
import { MARKETING_TYPE_OPTIONS } from '@/config/constant';
|
import {
|
||||||
|
MARKETING_DATE_FILTER_TYPE_OPTIONS,
|
||||||
|
MARKETING_TYPE_OPTIONS,
|
||||||
|
} from '@/config/constant';
|
||||||
import { httpClient } from '@/services/http/client';
|
import { httpClient } from '@/services/http/client';
|
||||||
import { BaseApiResponse } from '@/types/api/api-general';
|
import { BaseApiResponse } from '@/types/api/api-general';
|
||||||
import {
|
import {
|
||||||
@@ -84,6 +87,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
options: areaOptions,
|
options: areaOptions,
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
} = useSelect<Area>(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -98,6 +102,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -115,6 +120,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setWarehouseInputValue,
|
setInputValue: setWarehouseInputValue,
|
||||||
options: warehouseOptions,
|
options: warehouseOptions,
|
||||||
isLoadingOptions: isLoadingWarehouseOptions,
|
isLoadingOptions: isLoadingWarehouseOptions,
|
||||||
|
loadMore: loadMoreWarehouses,
|
||||||
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Warehouse>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -132,6 +138,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
setInputValue: setCustomerInputValue,
|
setInputValue: setCustomerInputValue,
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
isLoadingOptions: isLoadingCustomerOptions,
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const customerChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -150,6 +157,15 @@ const DailyMarketingReportContent = () => {
|
|||||||
updateFilter('end_date', e.target.value ? e.target.value : '');
|
updateFilter('end_date', e.target.value ? e.target.value : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [selectedMarketingDateFilterType, setSelectedMarketingDateFilterType] =
|
||||||
|
useState<OptionType | null>(null);
|
||||||
|
const marketingDateFilterTypeChangeHandler = (
|
||||||
|
val: OptionType | OptionType[] | null
|
||||||
|
) => {
|
||||||
|
setSelectedMarketingDateFilterType(val as OptionType);
|
||||||
|
updateFilter('filter_by', val ? ((val as OptionType).value as string) : '');
|
||||||
|
};
|
||||||
|
|
||||||
const [selectedMarketingType, setSelectedMarketingType] =
|
const [selectedMarketingType, setSelectedMarketingType] =
|
||||||
useState<OptionType | null>(null);
|
useState<OptionType | null>(null);
|
||||||
const marketingTypeChangeHandler = (
|
const marketingTypeChangeHandler = (
|
||||||
@@ -252,6 +268,23 @@ const DailyMarketingReportContent = () => {
|
|||||||
resetFilter();
|
resetFilter();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
tableFilterState.filter_by === 'realization_date' ||
|
||||||
|
tableFilterState.filter_by === 'so_date'
|
||||||
|
) {
|
||||||
|
setSelectedMarketingDateFilterType({
|
||||||
|
label:
|
||||||
|
tableFilterState.filter_by === 'realization_date'
|
||||||
|
? 'Tanggal Realisasi'
|
||||||
|
: 'Tanggal SO',
|
||||||
|
value: tableFilterState.filter_by,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedMarketingDateFilterType(null);
|
||||||
|
}
|
||||||
|
}, [tableFilterState.filter_by]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full border border-gray-200 p-4'>
|
<div className='w-full border border-gray-200 p-4'>
|
||||||
<div>
|
<div>
|
||||||
@@ -269,6 +302,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={areaChangeHandler}
|
onChange={areaChangeHandler}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -283,6 +317,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -297,6 +332,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedWarehouse}
|
value={selectedWarehouse}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
onInputChange={setWarehouseInputValue}
|
onInputChange={setWarehouseInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -311,6 +347,7 @@ const DailyMarketingReportContent = () => {
|
|||||||
value={selectedCustomer}
|
value={selectedCustomer}
|
||||||
onChange={customerChangeHandler}
|
onChange={customerChangeHandler}
|
||||||
onInputChange={setCustomerInputValue}
|
onInputChange={setCustomerInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -341,6 +378,18 @@ const DailyMarketingReportContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-12 gap-4'>
|
<div className='grid grid-cols-12 gap-4'>
|
||||||
|
<SelectInput
|
||||||
|
label='Filter Tanggal'
|
||||||
|
placeholder='Pilih Filter Tanggal'
|
||||||
|
options={MARKETING_DATE_FILTER_TYPE_OPTIONS}
|
||||||
|
value={selectedMarketingDateFilterType}
|
||||||
|
onChange={marketingDateFilterTypeChangeHandler}
|
||||||
|
isClearable
|
||||||
|
className={{
|
||||||
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Tipe Marketing'
|
label='Tipe Marketing'
|
||||||
placeholder='Pilih Tipe Marketing'
|
placeholder='Pilih Tipe Marketing'
|
||||||
|
|||||||
@@ -71,19 +71,22 @@ const DailyMarketingsTable = ({
|
|||||||
cell: (props) => `${props.row.original.aging_days} hari`,
|
cell: (props) => `${props.row.original.aging_days} hari`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'warehouse.name',
|
accessorKey: 'warehouse',
|
||||||
header: 'Gudang',
|
header: 'Gudang',
|
||||||
|
cell: ({ row }) => row.original.warehouse.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer',
|
||||||
header: 'Pelanggan',
|
header: 'Pelanggan',
|
||||||
|
cell: ({ row }) => row.original.customer.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'do_number',
|
accessorKey: 'do_number',
|
||||||
header: 'No. DO',
|
header: 'No. DO',
|
||||||
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'sales',
|
accessorKey: 'sales_person',
|
||||||
header: 'Sales/Marketing',
|
header: 'Sales/Marketing',
|
||||||
cell: (props) => props.row.original.sales.name,
|
cell: (props) => props.row.original.sales.name,
|
||||||
},
|
},
|
||||||
@@ -97,10 +100,12 @@ const DailyMarketingsTable = ({
|
|||||||
{
|
{
|
||||||
accessorKey: 'marketing_type',
|
accessorKey: 'marketing_type',
|
||||||
header: 'Marketing Type',
|
header: 'Marketing Type',
|
||||||
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'product.name',
|
accessorKey: 'product',
|
||||||
header: 'Produk',
|
header: 'Produk',
|
||||||
|
cell: ({ row }) => row.original.product.name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'qty',
|
accessorKey: 'qty',
|
||||||
@@ -115,12 +120,12 @@ const DailyMarketingsTable = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'average_weight_kg',
|
accessorKey: 'average_weight',
|
||||||
header: 'Bobot Rata-Rata (Kg)',
|
header: 'Bobot Rata-Rata (Kg)',
|
||||||
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
cell: (props) => formatNumber(props.row.original.average_weight_kg),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'total_weight_kg',
|
accessorKey: 'total_weight',
|
||||||
header: 'Bobot Total (Kg)',
|
header: 'Bobot Total (Kg)',
|
||||||
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
cell: (props) => formatNumber(props.row.original.total_weight_kg),
|
||||||
footer: () => {
|
footer: () => {
|
||||||
@@ -132,12 +137,12 @@ const DailyMarketingsTable = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'sales_price_per_kg',
|
accessorKey: 'sales_price',
|
||||||
header: 'Harga Jual (Rp)',
|
header: 'Harga Jual (Rp)',
|
||||||
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.sales_price_per_kg),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'hpp_price_per_kg',
|
accessorKey: 'hpp_price',
|
||||||
header: 'HPP (Rp)',
|
header: 'HPP (Rp)',
|
||||||
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
cell: (props) => formatCurrency(props.row.original.hpp_price_per_kg),
|
||||||
footer: () => {
|
footer: () => {
|
||||||
@@ -163,6 +168,8 @@ const DailyMarketingsTable = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log({ sorting });
|
||||||
|
|
||||||
if (sorting.length === 1) {
|
if (sorting.length === 1) {
|
||||||
onFilterByChange(sorting[0].id);
|
onFilterByChange(sorting[0].id);
|
||||||
onSortByChange(sorting[0].desc ? 'desc' : 'asc');
|
onSortByChange(sorting[0].desc ? 'desc' : 'asc');
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const MarketingReportContent = () => {
|
|||||||
const [activeTab, setActiveTab] = useState<string>('daily');
|
const [activeTab, setActiveTab] = useState<string>('daily');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeTabId={activeTab}
|
activeTabId={activeTab}
|
||||||
onTabChange={setActiveTab}
|
onTabChange={setActiveTab}
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ import MenuItem from '@/components/menu/MenuItem';
|
|||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
import { generateReportExpensePDF } from './pdf/ReportExpenseExport';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import {
|
||||||
|
KandangApi,
|
||||||
|
LocationApi,
|
||||||
|
NonstockApi,
|
||||||
|
SupplierApi,
|
||||||
|
} from '@/services/api/master-data';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
|
|
||||||
const ReportExpenseTable = () => {
|
const ReportExpenseTable = () => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
@@ -64,16 +73,33 @@ const ReportExpenseTable = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== SELECT OPTIONS =====
|
// ===== SELECT OPTIONS =====
|
||||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocation } =
|
const {
|
||||||
useSelect(`/master-data/locations`, 'id', 'name');
|
setInputValue: setLocationInputValue,
|
||||||
const { options: optionsSupplier, isLoadingOptions: isLoadingSupplier } =
|
options: locationOptions,
|
||||||
useSelect(`/master-data/suppliers`, 'id', 'name');
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
const { options: optionsKandang, isLoadingOptions: isLoadingKandang } =
|
loadMore: loadMoreLocations,
|
||||||
useSelect(`/master-data/kandangs`, 'id', 'name', '', {
|
} = useSelect<Location>(LocationApi.basePath, 'id', 'name');
|
||||||
location_id: filterState.location_id,
|
|
||||||
});
|
const {
|
||||||
const { options: optionsNonstock, isLoadingOptions: isLoadingNonstock } =
|
setInputValue: setSupplierInputValue,
|
||||||
useSelect(`/master-data/nonstocks`, 'id', 'name');
|
options: supplierOptions,
|
||||||
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setKandangInputValue,
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangOptions,
|
||||||
|
loadMore: loadMoreKandangs,
|
||||||
|
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setNonstockInputValue,
|
||||||
|
options: nonstockOptions,
|
||||||
|
isLoadingOptions: isLoadingNonstockOptions,
|
||||||
|
loadMore: loadMoreNonstocks,
|
||||||
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const categoryOptions = useMemo(
|
const categoryOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -86,31 +112,31 @@ const ReportExpenseTable = () => {
|
|||||||
// Mendapatkan value option select dari filter state
|
// Mendapatkan value option select dari filter state
|
||||||
const selectedLocation = useMemo(
|
const selectedLocation = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsLocation.find(
|
locationOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.location_id
|
(opt) => String(opt.value) === filterState.location_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsLocation, filterState.location_id]
|
[locationOptions, filterState.location_id]
|
||||||
);
|
);
|
||||||
const selectedSupplier = useMemo(
|
const selectedSupplier = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsSupplier.find(
|
supplierOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.supplier_id
|
(opt) => String(opt.value) === filterState.supplier_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsSupplier, filterState.supplier_id]
|
[supplierOptions, filterState.supplier_id]
|
||||||
);
|
);
|
||||||
const selectedKandang = useMemo(
|
const selectedKandang = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsKandang.find(
|
kandangOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.kandang_id
|
(opt) => String(opt.value) === filterState.kandang_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsKandang, filterState.kandang_id]
|
[kandangOptions, filterState.kandang_id]
|
||||||
);
|
);
|
||||||
const selectedNonstock = useMemo(
|
const selectedNonstock = useMemo(
|
||||||
() =>
|
() =>
|
||||||
optionsNonstock.find(
|
nonstockOptions.find(
|
||||||
(opt) => String(opt.value) === filterState.nonstock_id
|
(opt) => String(opt.value) === filterState.nonstock_id
|
||||||
) || null,
|
) || null,
|
||||||
[optionsNonstock, filterState.nonstock_id]
|
[nonstockOptions, filterState.nonstock_id]
|
||||||
);
|
);
|
||||||
const selectedCategory = useMemo(
|
const selectedCategory = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -756,38 +782,46 @@ const ReportExpenseTable = () => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
options={optionsLocation}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocation}
|
isLoading={isLoadingLocationOptions}
|
||||||
placeholder='Lokasi'
|
placeholder='Lokasi'
|
||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Kandang'
|
label='Kandang'
|
||||||
options={optionsKandang}
|
options={kandangOptions}
|
||||||
isLoading={isLoadingKandang}
|
isLoading={isLoadingKandangOptions}
|
||||||
placeholder='Kandang'
|
placeholder='Kandang'
|
||||||
value={selectedKandang}
|
value={selectedKandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
|
onInputChange={setKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangs}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Supplier'
|
label='Supplier'
|
||||||
options={optionsSupplier}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingSupplier}
|
isLoading={isLoadingSupplierOptions}
|
||||||
placeholder='Supplier'
|
placeholder='Supplier'
|
||||||
value={selectedSupplier}
|
value={selectedSupplier}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
label='Produk'
|
label='Produk'
|
||||||
options={optionsNonstock}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstock}
|
isLoading={isLoadingNonstockOptions}
|
||||||
placeholder='Produk'
|
placeholder='Produk'
|
||||||
value={selectedNonstock}
|
value={selectedNonstock}
|
||||||
onChange={nonstockChangeHandler}
|
onChange={nonstockChangeHandler}
|
||||||
|
onInputChange={setNonstockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstocks}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
isClearable
|
isClearable
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ const FinanceTabs = () => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
label: 'Kontrol Pembayaran Customer',
|
|
||||||
|
|
||||||
content: <CustomerPaymentTab />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
label: 'Rekapitulasi Hutang Ke Supplier',
|
label: 'Rekapitulasi Hutang Ke Supplier',
|
||||||
|
|
||||||
content: <DebtSupplierTab />,
|
content: <DebtSupplierTab />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
label: 'Kontrol Pembayaran Customer',
|
||||||
|
|
||||||
|
content: <CustomerPaymentTab />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -136,41 +136,132 @@ const pdfStyles = StyleSheet.create({
|
|||||||
backgroundColor: '#F0F0F0',
|
backgroundColor: '#F0F0F0',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
},
|
},
|
||||||
|
badge: {
|
||||||
|
backgroundColor: '#1f74bf',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 2,
|
||||||
|
fontSize: 7,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
badgeLunas: {
|
||||||
|
backgroundColor: '#1f74bf',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
badgeBelumLunas: {
|
||||||
|
backgroundColor: '#F97316',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
textError: {
|
||||||
|
color: '#DC2626',
|
||||||
|
},
|
||||||
|
parameterBadge: {
|
||||||
|
backgroundColor: '#F5F5F5',
|
||||||
|
color: '#333333',
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 8,
|
||||||
|
marginRight: 8,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
parameterContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface CustomerPaymentExportPDFParams {
|
interface CustomerPaymentExportPDFParams {
|
||||||
data: CustomerPaymentReport[];
|
data: CustomerPaymentReport[];
|
||||||
|
params?: {
|
||||||
|
customer_name?: string;
|
||||||
|
sales?: string;
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getParameterText = (
|
||||||
|
params?: CustomerPaymentExportPDFParams['params']
|
||||||
|
) => {
|
||||||
|
const paramsText = [];
|
||||||
|
|
||||||
|
if (params?.customer_name) {
|
||||||
|
paramsText.push(`Customer: ${params.customer_name}`);
|
||||||
|
} else {
|
||||||
|
paramsText.push('Semua Customer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.sales) {
|
||||||
|
paramsText.push(`Sales: ${params.sales}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params?.start_date && params?.end_date) {
|
||||||
|
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||||
|
const endDate = formatDate(params.end_date, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Periode: ${startDate} - ${endDate}`);
|
||||||
|
} else if (params?.start_date) {
|
||||||
|
const startDate = formatDate(params.start_date, 'DD MMM YYYY');
|
||||||
|
paramsText.push(`Tanggal: ${startDate}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = formatDate(new Date(), 'DD MMM YYYY HH:mm');
|
||||||
|
paramsText.push(`Dicetak: ${currentDate}`);
|
||||||
|
|
||||||
|
return paramsText;
|
||||||
|
};
|
||||||
|
|
||||||
const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
{params.data.map((customerReport, customerIndex) => (
|
{params.data.map((customerReport, customerIndex) => (
|
||||||
<Page
|
<Page
|
||||||
key={customerIndex}
|
key={customerIndex}
|
||||||
size='A4'
|
size='A3'
|
||||||
orientation='landscape'
|
orientation='landscape'
|
||||||
style={pdfStyles.page}
|
style={pdfStyles.page}
|
||||||
>
|
>
|
||||||
{/* Title and Customer Info */}
|
{/* Title and Parameters */}
|
||||||
<View style={pdfStyles.titleSection}>
|
<View style={pdfStyles.titleSection}>
|
||||||
<Text style={pdfStyles.mainTitle}>
|
<Text style={pdfStyles.mainTitle}>
|
||||||
Laporan > Kontrol Pembayaran Customer
|
Laporan > Kontrol Pembayaran Customer
|
||||||
</Text>
|
</Text>
|
||||||
|
<View style={pdfStyles.parameterContainer}>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Periode:{' '}
|
||||||
|
{params.params?.start_date
|
||||||
|
? formatDate(params.params.start_date, 'DD MMM YYYY')
|
||||||
|
: '-'}{' '}
|
||||||
|
s.d{' '}
|
||||||
|
{params.params?.end_date
|
||||||
|
? formatDate(params.params.end_date, 'DD MMM YYYY')
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>Filter Tanggal: Tanggal DO</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Customer: {params.params?.customer_name || 'Semua Customer'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={pdfStyles.parameterBadge}>
|
||||||
|
<Text>
|
||||||
|
Dicetak: {formatDate(new Date(), 'DD MMM YYYY HH:mm')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
<Text style={pdfStyles.supplierTitle}>
|
<Text style={pdfStyles.supplierTitle}>
|
||||||
{customerReport.customer.name}
|
{customerReport.customer.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={pdfStyles.supplierInfo}>
|
<Text style={pdfStyles.supplierInfo}>
|
||||||
{customerReport.customer.address || ''}
|
Alamat: {customerReport.customer.address || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
{customerReport.summary && (
|
|
||||||
<Text style={pdfStyles.supplierInfo}>
|
|
||||||
Total Saldo Piutang:{' '}
|
|
||||||
{formatCurrency(
|
|
||||||
customerReport.summary.total_accounts_receivable
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Table */}
|
{/* Table */}
|
||||||
@@ -181,10 +272,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>No</Text>
|
<Text>No</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
<Text>Tgl DO/Bayar</Text>
|
<Text>Tanggal DO</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
<Text>Tgl Realisasi</Text>
|
<Text>Tanggal Realisasi</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 0.8 }]}>
|
||||||
<Text>Aging</Text>
|
<Text>Aging</Text>
|
||||||
@@ -193,16 +284,16 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>Referensi</Text>
|
<Text>Referensi</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.2 }]}>
|
||||||
<Text>No. Polisi</Text>
|
<Text>No Polisi</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
<Text>Qty</Text>
|
<Text>Qty</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1 }]}>
|
||||||
<Text>Berat (Kg)</Text>
|
<Text>Berat</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
<Text>AVG</Text>
|
<Text>Rata-Rata</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Harga Awal</Text>
|
<Text>Harga Awal</Text>
|
||||||
@@ -214,7 +305,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>Harga Akhir</Text>
|
<Text>Harga Akhir</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 0.8 }]}>
|
||||||
<Text>PPN (%)</Text>
|
<Text>Pajak</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Total</Text>
|
<Text>Total</Text>
|
||||||
@@ -223,10 +314,10 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>Pembayaran</Text>
|
<Text>Pembayaran</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.2 }]}>
|
||||||
<Text>Saldo Piutang</Text>
|
<Text>Saldo</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1.5 }]}>
|
||||||
<Text>Ket</Text>
|
<Text>Keterangan</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Pengambilan</Text>
|
<Text>Pengambilan</Text>
|
||||||
@@ -301,10 +392,29 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
<Text>{formatCurrency(item.payment)}</Text>
|
<Text>{formatCurrency(item.payment)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>{formatCurrency(item.accounts_receivable)}</Text>
|
<Text style={pdfStyles.textError}>
|
||||||
|
{formatCurrency(item.accounts_receivable)}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
<Text>{item.notes || '-'}</Text>
|
{item.notes ? (
|
||||||
|
<Text>{item.notes}</Text>
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.badge,
|
||||||
|
item.accounts_receivable === 0
|
||||||
|
? pdfStyles.badgeLunas
|
||||||
|
: pdfStyles.badgeBelumLunas,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{item.accounts_receivable === 0
|
||||||
|
? 'Lunas'
|
||||||
|
: 'Belum Lunas'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text>{item.pickup_info || '-'}</Text>
|
<Text>{item.pickup_info || '-'}</Text>
|
||||||
@@ -378,7 +488,7 @@ const createPDFDocument = (params: CustomerPaymentExportPDFParams) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
<View style={[pdfStyles.tableCellRight, { flex: 1.2 }]}>
|
||||||
<Text>
|
<Text style={pdfStyles.textError}>
|
||||||
{formatCurrency(
|
{formatCurrency(
|
||||||
customerReport.summary.total_accounts_receivable
|
customerReport.summary.total_accounts_receivable
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<Text>No. PO</Text>
|
<Text>No. PO</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Tgl Terima</Text>
|
<Text>Tgl Terima/Bayar</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Tgl PO</Text>
|
<Text>Tgl PO</Text>
|
||||||
@@ -191,19 +191,19 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<Text>Gudang</Text>
|
<Text>Gudang</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Tgl Jatuh Tempo</Text>
|
<Text>Jatuh Tempo</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Status Jatuh Tempo</Text>
|
<Text>Status Jatuh Tempo</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
<Text>Total Harga</Text>
|
<Text>Nominal Pembelian (Rp)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
<Text>Pembayaran</Text>
|
<Text>Pembayaran (Rp)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
<View style={[pdfStyles.tableCellHeaderRight, { flex: 1.5 }]}>
|
||||||
<Text>Hutang</Text>
|
<Text>Sisa Saldo Hutang (Rp)</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCellHeader, { flex: 1 }]}>
|
||||||
<Text>Status</Text>
|
<Text>Status</Text>
|
||||||
@@ -213,6 +213,65 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Initial Balance Row */}
|
||||||
|
<View style={[pdfStyles.tableRow, pdfStyles.tableBorderBottom]}>
|
||||||
|
<View style={[pdfStyles.tableCellNo, { flex: 0.5 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1.5 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 0.6 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellCenter, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCellRight, { flex: 1.5 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
pdfStyles.tableCellRight,
|
||||||
|
{
|
||||||
|
flex: 1.5,
|
||||||
|
color: supplierReport.initial_balance < 0 ? 'red' : 'black',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{formatCurrency(supplierReport.initial_balance || 0)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
|
<Text></Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Table Body */}
|
{/* Table Body */}
|
||||||
{supplierReport.rows.map((item, index) => (
|
{supplierReport.rows.map((item, index) => (
|
||||||
<View
|
<View
|
||||||
@@ -297,10 +356,10 @@ const createPDFDocument = (params: DebtSupplierExportPDFParams) => {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pdfStyles.tableCellRight,
|
pdfStyles.tableCellRight,
|
||||||
{ flex: 1.5, color: item.debt_price < 0 ? 'red' : 'black' },
|
{ flex: 1.5, color: item.balance < 0 ? 'red' : 'black' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text>{formatCurrency(item.debt_price)}</Text>
|
<Text>{formatCurrency(item.balance)}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
<View style={[pdfStyles.tableCell, { flex: 1 }]}>
|
||||||
<Text>{item.status || '-'}</Text>
|
<Text>{item.status || '-'}</Text>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { formatDate } from '@/lib/helper';
|
import { formatDate } from '@/lib/helper';
|
||||||
import { DebtSupplier } from '@/types/api/report/debt-supplier';
|
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
||||||
|
|
||||||
interface DebtSupplierExportExcelParams {
|
interface DebtSupplierExportExcelParams {
|
||||||
data: DebtSupplier[];
|
data: DebtSupplier[];
|
||||||
@@ -21,12 +21,29 @@ export const generateDebtSupplierExcel = (
|
|||||||
const supplierData = supplierReport.rows;
|
const supplierData = supplierReport.rows;
|
||||||
const supplierName = supplierReport.supplier.name || 'Unknown Supplier';
|
const supplierName = supplierReport.supplier.name || 'Unknown Supplier';
|
||||||
|
|
||||||
const excelData: { [key: string]: string | number }[] = supplierData.map(
|
const excelData: { [key: string]: string | number }[] = [
|
||||||
(item, index) => ({
|
{
|
||||||
|
No: '',
|
||||||
|
'Nomor PR': '',
|
||||||
|
'Nomor PO': '',
|
||||||
|
'Tanggal Terima/Bayar': '',
|
||||||
|
'Tanggal PO': '',
|
||||||
|
'Aging (Hari)': '',
|
||||||
|
Area: '',
|
||||||
|
Gudang: '',
|
||||||
|
'Jatuh Tempo': '',
|
||||||
|
'Status Jatuh Tempo': '',
|
||||||
|
'Nominal Pembelian (Rp)': '',
|
||||||
|
'Pembayaran (Rp)': '',
|
||||||
|
'Sisa Saldo Hutang (Rp)': supplierReport.initial_balance || 0,
|
||||||
|
Status: '',
|
||||||
|
'Nomor Perjalanan': '',
|
||||||
|
},
|
||||||
|
...supplierData.map((item, index) => ({
|
||||||
No: index + 1,
|
No: index + 1,
|
||||||
'Nomor PR': item.pr_number || '',
|
'Nomor PR': item.pr_number || '',
|
||||||
'Nomor PO': item.po_number || '',
|
'Nomor PO': item.po_number || '',
|
||||||
'Tanggal Terima': item.received_date
|
'Tanggal Terima/Bayar': item.received_date
|
||||||
? item.received_date != '-'
|
? item.received_date != '-'
|
||||||
? formatDate(item.received_date, 'MM/DD/YYYY')
|
? formatDate(item.received_date, 'MM/DD/YYYY')
|
||||||
: '-'
|
: '-'
|
||||||
@@ -39,35 +56,35 @@ export const generateDebtSupplierExcel = (
|
|||||||
'Aging (Hari)': item.aging || 0,
|
'Aging (Hari)': item.aging || 0,
|
||||||
Area: item.area?.name || '',
|
Area: item.area?.name || '',
|
||||||
Gudang: item.warehouse?.name || '',
|
Gudang: item.warehouse?.name || '',
|
||||||
'Tanggal Jatuh Tempo': item.due_date
|
'Jatuh Tempo': item.due_date
|
||||||
? item.due_date != '-'
|
? item.due_date != '-'
|
||||||
? formatDate(item.due_date, 'MM/DD/YYYY')
|
? formatDate(item.due_date, 'MM/DD/YYYY')
|
||||||
: '-'
|
: '-'
|
||||||
: '-',
|
: '-',
|
||||||
'Status Jatuh Tempo': item.due_status || '',
|
'Status Jatuh Tempo': item.due_status || '',
|
||||||
'Total Harga': item.total_price || 0,
|
'Nominal Pembelian (Rp)': item.total_price || 0,
|
||||||
'Harga Pembayaran': item.payment_price || 0,
|
'Pembayaran (Rp)': item.payment_price || 0,
|
||||||
'Harga Hutang': item.debt_price || 0,
|
'Sisa Saldo Hutang (Rp)': item.debt_price || 0,
|
||||||
Status: item.status || '',
|
Status: item.status || '',
|
||||||
'Nomor Perjalanan': item.travel_number || '',
|
'Nomor Perjalanan': item.travel_number || '',
|
||||||
})
|
})),
|
||||||
);
|
];
|
||||||
|
|
||||||
if (supplierReport.total) {
|
if (supplierReport.total) {
|
||||||
excelData.push({
|
excelData.push({
|
||||||
No: 'Total',
|
No: 'Total',
|
||||||
'Nomor PR': '',
|
'Nomor PR': '',
|
||||||
'Nomor PO': '',
|
'Nomor PO': '',
|
||||||
'Tanggal Terima': '',
|
'Tanggal Terima/Bayar': '',
|
||||||
'Tanggal PO': '',
|
'Tanggal PO': '',
|
||||||
'Aging (Hari)': supplierReport.total.aging || 0,
|
'Aging (Hari)': supplierReport.total.aging || 0,
|
||||||
Area: '',
|
Area: '',
|
||||||
Gudang: '',
|
Gudang: '',
|
||||||
'Tanggal Jatuh Tempo': '',
|
'Jatuh Tempo': '',
|
||||||
'Status Jatuh Tempo': '',
|
'Status Jatuh Tempo': '',
|
||||||
'Total Harga': supplierReport.total.total_price || 0,
|
'Nominal Pembelian (Rp)': supplierReport.total.total_price || 0,
|
||||||
'Harga Pembayaran': supplierReport.total.payment_price || 0,
|
'Pembayaran (Rp)': supplierReport.total.payment_price || 0,
|
||||||
'Harga Hutang': supplierReport.total.debt_price || 0,
|
'Sisa Saldo Hutang (Rp)': supplierReport.total.debt_price || 0,
|
||||||
Status: '',
|
Status: '',
|
||||||
'Nomor Perjalanan': '',
|
'Nomor Perjalanan': '',
|
||||||
});
|
});
|
||||||
@@ -79,16 +96,16 @@ export const generateDebtSupplierExcel = (
|
|||||||
{ wch: 5 }, // No
|
{ wch: 5 }, // No
|
||||||
{ wch: 15 }, // Nomor PR
|
{ wch: 15 }, // Nomor PR
|
||||||
{ wch: 15 }, // Nomor PO
|
{ wch: 15 }, // Nomor PO
|
||||||
{ wch: 15 }, // Tanggal PR
|
{ wch: 15 }, // Tanggal Terima/Bayar
|
||||||
{ wch: 15 }, // Tanggal PO
|
{ wch: 15 }, // Tanggal PO
|
||||||
{ wch: 12 }, // Aging
|
{ wch: 12 }, // Aging
|
||||||
{ wch: 15 }, // Area
|
{ wch: 15 }, // Area
|
||||||
{ wch: 15 }, // Gudang
|
{ wch: 15 }, // Gudang
|
||||||
{ wch: 18 }, // Tanggal Jatuh Tempo
|
{ wch: 18 }, // Jatuh Tempo
|
||||||
{ wch: 18 }, // Status Jatuh Tempo
|
{ wch: 18 }, // Status Jatuh Tempo
|
||||||
{ wch: 15 }, // Total Harga
|
{ wch: 15 }, // Nominal Pembelian (Rp)
|
||||||
{ wch: 15 }, // Harga Pembayaran
|
{ wch: 15 }, // Pembayaran (Rp)
|
||||||
{ wch: 15 }, // Harga Hutang
|
{ wch: 15 }, // Sisa Saldo Hutang (Rp)
|
||||||
{ wch: 12 }, // Status
|
{ wch: 12 }, // Status
|
||||||
{ wch: 15 }, // Nomor Perjalanan
|
{ wch: 15 }, // Nomor Perjalanan
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
export type DebtSupplierFilterType = {
|
||||||
|
startDate: string | null | undefined;
|
||||||
|
endDate: string | null | undefined;
|
||||||
|
supplierIds: OptionType[] | null | undefined;
|
||||||
|
filterBy: OptionType | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DebtSupplierFilterSchema: yup.ObjectSchema<DebtSupplierFilterType> =
|
||||||
|
yup.object({
|
||||||
|
startDate: yup.string().optional().notRequired(),
|
||||||
|
endDate: yup.string().optional().notRequired(),
|
||||||
|
supplierIds: yup
|
||||||
|
.array()
|
||||||
|
.of(
|
||||||
|
yup.object({
|
||||||
|
value: yup.mixed<string | number>().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.notRequired(),
|
||||||
|
filterBy: yup
|
||||||
|
.object({
|
||||||
|
value: yup.mixed<string | number>().required(),
|
||||||
|
label: yup.string().required(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.notRequired(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DebtSupplierFilterValues = yup.InferType<
|
||||||
|
typeof DebtSupplierFilterSchema
|
||||||
|
>;
|
||||||
@@ -2,13 +2,16 @@ import { useState, useMemo, useCallback } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
import SelectInput, {
|
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';
|
||||||
@@ -17,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';
|
||||||
@@ -36,38 +38,74 @@ 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 [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const { options: customerOptions, isLoadingOptions: isLoadingCustomers } =
|
const {
|
||||||
useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
options: customerOptions,
|
||||||
|
setInputValue: setCustomerInputValue,
|
||||||
|
isLoadingOptions: isLoadingCustomers,
|
||||||
|
loadMore: loadMoreCustomers,
|
||||||
|
hasMore: hasMoreCustomers,
|
||||||
|
} = useSelect(CustomerApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
const salesOptions = useMemo(
|
const {
|
||||||
() => [
|
options: salesOptions,
|
||||||
{ value: 'Sales A', label: 'Sales A' },
|
setInputValue: setSalesInputValue,
|
||||||
{ value: 'Sales B', label: 'Sales B' },
|
isLoadingOptions: isLoadingSales,
|
||||||
{ value: 'Sales C', label: 'Sales C' },
|
loadMore: loadMoreSales,
|
||||||
// TODO: Fetch sales options from API
|
hasMore: hasMoreSales,
|
||||||
],
|
} = useSelect(UserApi.basePath, 'id', 'name', 'search');
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const dataTypeOptions = useMemo(
|
const dataTypeOptions = useMemo(
|
||||||
() => [{ value: 'do_date', label: 'Tanggal Jual' }],
|
() => [{ value: 'do_date', label: 'Tanggal Jual' }],
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getPaymentStatusColor = (notes: string) => {
|
||||||
|
const normalizedValue = notes.toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedValue === 'lunas') {
|
||||||
|
return 'bg-info/10 text-info border-info';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue.includes('belum')) {
|
||||||
|
return 'bg-warning/10 text-warning border-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'bg-gray-100 text-gray-600 border-gray-300';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentStatusIndicatorColor = (notes: string) => {
|
||||||
|
const normalizedValue = notes.toLowerCase();
|
||||||
|
|
||||||
|
if (normalizedValue === 'lunas') {
|
||||||
|
return 'bg-info';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedValue.includes('belum')) {
|
||||||
|
return 'bg-warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'bg-gray-400';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPaymentStatusText = (notes: string) => {
|
||||||
|
return notes;
|
||||||
|
};
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleResetFilters = useCallback(() => {
|
const handleResetFilters = useCallback(() => {
|
||||||
setIsSubmitted(false);
|
setIsSubmitted(false);
|
||||||
@@ -75,27 +113,48 @@ const CustomerPaymentTab = () => {
|
|||||||
setFilterSales([]);
|
setFilterSales([]);
|
||||||
setFilterStartDate('');
|
setFilterStartDate('');
|
||||||
setFilterEndDate('');
|
setFilterEndDate('');
|
||||||
setFilterErrors({});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleApplyFilters = useCallback(() => {
|
const handleApplyFilters = useCallback(() => {
|
||||||
const errors: Record<string, string> = {};
|
setIsSubmitted(true);
|
||||||
|
setCurrentPage(1);
|
||||||
|
filterModal.closeModal();
|
||||||
|
}, [filterModal]);
|
||||||
|
|
||||||
if (!filterStartDate) {
|
// ===== ACTIVE FILTERS COUNT =====
|
||||||
errors.start_date = 'Tanggal mulai wajib diisi';
|
const activeFiltersCount = useMemo(() => {
|
||||||
}
|
let count = 0;
|
||||||
if (!filterEndDate) {
|
|
||||||
errors.end_date = 'Tanggal akhir wajib diisi';
|
// Date filter (start_date + end_date = 1 filter)
|
||||||
|
if (filterStartDate || filterEndDate) {
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilterErrors(errors);
|
// Customer filter
|
||||||
|
if (filterCustomer.length > 0) {
|
||||||
if (Object.keys(errors).length === 0) {
|
count += 1;
|
||||||
setIsSubmitted(true);
|
|
||||||
setCurrentPage(1);
|
|
||||||
filterModal.closeModal();
|
|
||||||
}
|
}
|
||||||
}, [filterModal, filterStartDate, filterEndDate]);
|
|
||||||
|
// 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(
|
||||||
@@ -106,7 +165,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,
|
||||||
@@ -123,7 +182,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,
|
||||||
@@ -140,11 +199,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
|
||||||
@@ -154,7 +208,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,
|
||||||
@@ -167,7 +221,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,
|
||||||
@@ -218,7 +272,22 @@ const CustomerPaymentTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await generateCustomerPaymentPDF({ data: allDataForExport });
|
await generateCustomerPaymentPDF({
|
||||||
|
data: allDataForExport,
|
||||||
|
params: {
|
||||||
|
customer_name:
|
||||||
|
filterCustomer.length > 0
|
||||||
|
? filterCustomer.map((c) => c.label).join(', ')
|
||||||
|
: undefined,
|
||||||
|
sales:
|
||||||
|
filterSales.length > 0
|
||||||
|
? filterSales.map((s) => s.label).join(', ')
|
||||||
|
: undefined,
|
||||||
|
start_date: filterStartDate || undefined,
|
||||||
|
end_date: filterEndDate || undefined,
|
||||||
|
filter_by: 'do_date',
|
||||||
|
},
|
||||||
|
});
|
||||||
toast.success('PDF berhasil dibuat dan diunduh.');
|
toast.success('PDF berhasil dibuat dan diunduh.');
|
||||||
} catch {
|
} catch {
|
||||||
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
toast.error('Gagal membuat PDF. Silakan coba lagi.');
|
||||||
@@ -227,27 +296,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]>[] => {
|
||||||
@@ -435,7 +483,9 @@ const CustomerPaymentTab = () => {
|
|||||||
accessorKey: 'accounts_receivable',
|
accessorKey: 'accounts_receivable',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.accounts_receivable;
|
const value = props.row.original.accounts_receivable;
|
||||||
return <div className='text-right'>{formatCurrency(value)}</div>;
|
return (
|
||||||
|
<div className='text-right text-error'>{formatCurrency(value)}</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
footer: () => (
|
footer: () => (
|
||||||
<div className='text-right font-semibold text-gray-900'>
|
<div className='text-right font-semibold text-gray-900'>
|
||||||
@@ -449,7 +499,23 @@ const CustomerPaymentTab = () => {
|
|||||||
accessorKey: 'notes',
|
accessorKey: 'notes',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.notes;
|
const value = props.row.original.notes;
|
||||||
return value || '-';
|
|
||||||
|
if (!value) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
statusIndicator={true}
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge: `rounded-xl justify-start border border-gray-200 ${getPaymentStatusColor(value)}`,
|
||||||
|
status: getPaymentStatusIndicatorColor(value),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{getPaymentStatusText(value)}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -481,14 +547,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}
|
||||||
@@ -499,7 +588,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>
|
||||||
@@ -538,15 +627,9 @@ const CustomerPaymentTab = () => {
|
|||||||
value={filterStartDate}
|
value={filterStartDate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilterStartDate(e.target.value);
|
setFilterStartDate(e.target.value);
|
||||||
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
|
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
{filterErrors.start_date && (
|
|
||||||
<p className='text-red-500 text-sm mt-1'>
|
|
||||||
{filterErrors.start_date}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -556,23 +639,16 @@ const CustomerPaymentTab = () => {
|
|||||||
value={filterEndDate}
|
value={filterEndDate}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilterEndDate(e.target.value);
|
setFilterEndDate(e.target.value);
|
||||||
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
|
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
{filterErrors.end_date && (
|
|
||||||
<p className='text-red-500 text-sm mt-1'>
|
|
||||||
{filterErrors.end_date}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</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) => {
|
||||||
@@ -580,23 +656,27 @@ const CustomerPaymentTab = () => {
|
|||||||
Array.isArray(val) ? val : val ? [val] : []
|
Array.isArray(val) ? val : val ? [val] : []
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setCustomerInputValue}
|
||||||
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] : []);
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setSalesInputValue}
|
||||||
|
isLoading={isLoadingSales}
|
||||||
isClearable
|
isClearable
|
||||||
|
onMenuScrollToBottom={loadMoreSales}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -659,15 +739,18 @@ const CustomerPaymentTab = () => {
|
|||||||
total_accounts_receivable: 0,
|
total_accounts_receivable: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const totalAccountsReceivable = summary.total_accounts_receivable;
|
|
||||||
const tableColumns = getTableColumns(summary);
|
const tableColumns = getTableColumns(summary);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={customerReport.customer.id}
|
key={customerReport.customer.id}
|
||||||
title={customerReport.customer.name}
|
title={customerReport.customer.name}
|
||||||
subtitle={`${customerReport.customer.address || ''}\nSaldo Piutang: ${formatCurrency(totalAccountsReceivable)}`}
|
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}
|
||||||
>
|
>
|
||||||
@@ -678,7 +761,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:
|
||||||
@@ -700,20 +783,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import Table from '@/components/Table';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { DebtRow, DebtSupplier } from '@/types/api/report/debt-supplier';
|
import {
|
||||||
|
DebtRow,
|
||||||
|
DebtSupplier,
|
||||||
|
DebtSupplierFilter,
|
||||||
|
} from '@/types/api/report/debt-supplier';
|
||||||
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
import { generateDebtSupplierExcel } from '@/components/pages/report/finance/export/DebtSupplierExportXLSX';
|
||||||
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
import { generateDebtSupplierPDF } from '@/components/pages/report/finance/export/DebtSupllierExportPDF';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
@@ -21,8 +25,15 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
import { DebtSupplierApi } from '@/services/api/report/debt-supplier';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import {
|
||||||
|
DebtSupplierFilterSchema,
|
||||||
|
DebtSupplierFilterType,
|
||||||
|
} from '@/components/pages/report/finance/filter/DebtSupplierFilter';
|
||||||
|
import { getFilledFormikValuesCount } from '@/lib/formik-helper';
|
||||||
|
import ButtonFilter from '@/components/helper/ButtonFilter';
|
||||||
|
import { Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
const DebtSupplierTab = () => {
|
const DebtSupplierTab = () => {
|
||||||
// ===== STATE MANAGEMENT =====
|
// ===== STATE MANAGEMENT =====
|
||||||
@@ -30,26 +41,23 @@ const DebtSupplierTab = () => {
|
|||||||
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
const [isExcelExportLoading, setIsExcelExportLoading] = useState(false);
|
||||||
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
const isAnyExportLoading = isPdfExportLoading || isExcelExportLoading;
|
||||||
|
|
||||||
// ===== PAGINATION STATE =====
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [pageSize, setPageSize] = useState(10);
|
|
||||||
|
|
||||||
// ===== SUBMISSION STATE =====
|
// ===== SUBMISSION STATE =====
|
||||||
|
const [filterParams, setFilterParams] = useState<DebtSupplierFilter>({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||||
|
|
||||||
// ===== FILTER STATE =====
|
|
||||||
const [filterSupplier, setFilterSupplier] = useState<OptionType[]>([]);
|
|
||||||
const [filterStartDate, setFilterStartDate] = useState('');
|
|
||||||
const [filterEndDate, setFilterEndDate] = useState('');
|
|
||||||
const [filterDateType, setFilterDateType] = useState<OptionType>();
|
|
||||||
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
const { options: supplierOptions, isLoadingOptions: isLoadingSuppliers } =
|
const {
|
||||||
useSelect(SupplierApi.basePath, 'id', 'name', '', {
|
setInputValue: setSupplierInputValue,
|
||||||
limit: 'limit',
|
options: supplierOptions,
|
||||||
});
|
isLoadingOptions: isLoadingSupplierOptions,
|
||||||
|
loadMore: loadMoreSuppliers,
|
||||||
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const dataTypeOptions = useMemo(
|
const dataTypeOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -59,48 +67,51 @@ const DebtSupplierTab = () => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ===== FILTER HANDLERS =====
|
const handleFilterModalOpen = () => {
|
||||||
const handleResetFilters = useCallback(() => {
|
filterModal.openModal();
|
||||||
setIsSubmitted(false);
|
};
|
||||||
setFilterSupplier([]);
|
|
||||||
setFilterStartDate('');
|
|
||||||
setFilterEndDate('');
|
|
||||||
setFilterErrors({});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleApplyFilters = useCallback(() => {
|
// ===== FORMIK SETUP =====
|
||||||
const errors: Record<string, string> = {};
|
const formik = useFormik<DebtSupplierFilterType>({
|
||||||
|
initialValues: {
|
||||||
if (!filterStartDate) {
|
startDate: null,
|
||||||
errors.start_date = 'Tanggal mulai wajib diisi';
|
endDate: null,
|
||||||
}
|
supplierIds: null,
|
||||||
if (!filterEndDate) {
|
filterBy: null,
|
||||||
errors.end_date = 'Tanggal akhir wajib diisi';
|
},
|
||||||
}
|
validationSchema: DebtSupplierFilterSchema,
|
||||||
|
onSubmit: (values) => {
|
||||||
setFilterErrors(errors);
|
setFilterParams({
|
||||||
|
start_date: values.startDate?.toString() || undefined,
|
||||||
if (Object.keys(errors).length === 0) {
|
end_date: values.endDate?.toString() || undefined,
|
||||||
setIsSubmitted(true);
|
supplier_ids:
|
||||||
setCurrentPage(1);
|
values.supplierIds?.map((v) => String(v.value)).join(',') ||
|
||||||
|
undefined,
|
||||||
|
filter_by: values.filterBy?.value?.toString() || undefined,
|
||||||
|
});
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
}
|
setIsSubmitted(true);
|
||||||
}, [filterModal, filterStartDate, filterEndDate]);
|
},
|
||||||
|
onReset: (values) => {
|
||||||
|
setFilterParams({
|
||||||
|
start_date: undefined,
|
||||||
|
end_date: undefined,
|
||||||
|
supplier_ids: undefined,
|
||||||
|
filter_by: undefined,
|
||||||
|
});
|
||||||
|
setIsSubmitted(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ===== DATA FETCHING =====
|
// ===== DATA FETCHING =====
|
||||||
const { data: debtSupplier, isLoading } = useSWR(
|
const { data: debtSupplier, isLoading } = useSWR(
|
||||||
isSubmitted
|
isSubmitted
|
||||||
? () => {
|
? () => {
|
||||||
const params = {
|
const params = {
|
||||||
supplier_ids:
|
supplier_ids: filterParams.supplier_ids,
|
||||||
filterSupplier.length > 0
|
filter_by: filterParams.filter_by,
|
||||||
? filterSupplier.map((v) => String(v.value)).join(',')
|
start_date: filterParams.start_date,
|
||||||
: undefined,
|
end_date: filterParams.end_date,
|
||||||
filter_by: filterDateType?.value,
|
|
||||||
start_date: filterStartDate || undefined,
|
|
||||||
end_date: filterEndDate || undefined,
|
|
||||||
page: currentPage,
|
|
||||||
limit: pageSize,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return ['debt-supplier-report', params];
|
return ['debt-supplier-report', params];
|
||||||
@@ -109,11 +120,9 @@ const DebtSupplierTab = () => {
|
|||||||
([, params]) =>
|
([, params]) =>
|
||||||
DebtSupplierApi.getDebtSupplierReport(
|
DebtSupplierApi.getDebtSupplierReport(
|
||||||
params.supplier_ids,
|
params.supplier_ids,
|
||||||
params.filter_by?.toString(),
|
params.filter_by,
|
||||||
params.start_date,
|
params.start_date,
|
||||||
params.end_date,
|
params.end_date
|
||||||
params.page,
|
|
||||||
params.limit
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -135,13 +144,15 @@ const DebtSupplierTab = () => {
|
|||||||
> => {
|
> => {
|
||||||
const params = {
|
const params = {
|
||||||
supplier_ids:
|
supplier_ids:
|
||||||
filterSupplier.length > 0
|
formik.values.supplierIds && formik.values.supplierIds.length > 0
|
||||||
? filterSupplier.map((v) => String(v.value)).join(',')
|
? formik.values.supplierIds.map((v) => String(v.value)).join(',')
|
||||||
: undefined,
|
: undefined,
|
||||||
filter_by: filterDateType?.value?.toString(),
|
filter_by: formik.values.filterBy?.value?.toString() || undefined,
|
||||||
start_date: filterStartDate || undefined,
|
start_date: formik.values.startDate || undefined,
|
||||||
end_date: filterEndDate || undefined,
|
end_date: formik.values.endDate || undefined,
|
||||||
date_type: filterDateType ? filterDateType.value : undefined,
|
date_type: formik.values.filterBy
|
||||||
|
? formik.values.filterBy.value
|
||||||
|
: undefined,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
page: 1,
|
page: 1,
|
||||||
};
|
};
|
||||||
@@ -150,15 +161,18 @@ const DebtSupplierTab = () => {
|
|||||||
params.supplier_ids,
|
params.supplier_ids,
|
||||||
params.filter_by,
|
params.filter_by,
|
||||||
params.start_date,
|
params.start_date,
|
||||||
params.end_date,
|
params.end_date
|
||||||
params.page,
|
|
||||||
params.limit
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return isResponseSuccess(response)
|
return isResponseSuccess(response)
|
||||||
? (response.data as unknown as DebtSupplier[])
|
? (response.data as unknown as DebtSupplier[])
|
||||||
: null;
|
: null;
|
||||||
}, [filterSupplier, filterStartDate, filterEndDate]);
|
}, [
|
||||||
|
formik.values.supplierIds,
|
||||||
|
formik.values.startDate,
|
||||||
|
formik.values.endDate,
|
||||||
|
formik.values.filterBy,
|
||||||
|
]);
|
||||||
|
|
||||||
// ===== EXPORT HANDLERS =====
|
// ===== EXPORT HANDLERS =====
|
||||||
const handleExportExcel = useCallback(async () => {
|
const handleExportExcel = useCallback(async () => {
|
||||||
@@ -207,37 +221,18 @@ const DebtSupplierTab = () => {
|
|||||||
}
|
}
|
||||||
}, [debtSupplierExport]);
|
}, [debtSupplierExport]);
|
||||||
|
|
||||||
// ===== PAGINATION HANDLERS =====
|
|
||||||
const handlePageChange = (page: number) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowChange = (pageSize: number) => {
|
|
||||||
setPageSize(pageSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextPage = () => {
|
|
||||||
if (meta && currentPage < meta.total_pages) {
|
|
||||||
setCurrentPage(currentPage + 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrevPage = () => {
|
|
||||||
if (currentPage > 1) {
|
|
||||||
setCurrentPage(currentPage - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
|
const getTableColumns = (supplier: DebtSupplier): ColumnDef<DebtRow>[] => [
|
||||||
{
|
{
|
||||||
id: 'no',
|
id: 'no',
|
||||||
header: 'No',
|
header: 'No',
|
||||||
cell: (props) => props.row.index + 1,
|
enableSorting: false,
|
||||||
|
cell: (props) => props.row.index,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'pr_number',
|
id: 'pr_number',
|
||||||
header: 'Nomor PR',
|
header: 'Nomor PR',
|
||||||
accessorKey: 'pr_number',
|
accessorKey: 'pr_number',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.pr_number;
|
const value = props.row.original.pr_number;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -247,6 +242,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'po_number',
|
id: 'po_number',
|
||||||
header: 'Nomor PO',
|
header: 'Nomor PO',
|
||||||
accessorKey: 'po_number',
|
accessorKey: 'po_number',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.po_number;
|
const value = props.row.original.po_number;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -254,8 +250,9 @@ const DebtSupplierTab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'received_date',
|
id: 'received_date',
|
||||||
header: 'Tanggal Terima',
|
header: 'Tanggal Terima/Bayar',
|
||||||
accessorKey: 'received_date',
|
accessorKey: 'received_date',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.received_date;
|
const value = props.row.original.received_date;
|
||||||
return value
|
return value
|
||||||
@@ -269,6 +266,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'po_date',
|
id: 'po_date',
|
||||||
header: 'Tanggal PO',
|
header: 'Tanggal PO',
|
||||||
accessorKey: 'po_date',
|
accessorKey: 'po_date',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.po_date;
|
const value = props.row.original.po_date;
|
||||||
return value
|
return value
|
||||||
@@ -282,6 +280,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'aging',
|
id: 'aging',
|
||||||
header: 'Aging',
|
header: 'Aging',
|
||||||
accessorKey: 'aging',
|
accessorKey: 'aging',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.aging;
|
const value = props.row.original.aging;
|
||||||
return <div className='text-center'>{formatNumber(value)} Hari</div>;
|
return <div className='text-center'>{formatNumber(value)} Hari</div>;
|
||||||
@@ -295,6 +294,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'area',
|
id: 'area',
|
||||||
header: 'Area',
|
header: 'Area',
|
||||||
accessorKey: 'area',
|
accessorKey: 'area',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.area?.name;
|
const value = props.row.original.area?.name;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -304,6 +304,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'warehouse',
|
id: 'warehouse',
|
||||||
header: 'Gudang',
|
header: 'Gudang',
|
||||||
accessorKey: 'warehouse',
|
accessorKey: 'warehouse',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.warehouse?.name;
|
const value = props.row.original.warehouse?.name;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -311,8 +312,9 @@ const DebtSupplierTab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'due_date',
|
id: 'due_date',
|
||||||
header: 'Tanggal Jatuh Tempo',
|
header: 'Jatuh Tempo',
|
||||||
accessorKey: 'due_date',
|
accessorKey: 'due_date',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.due_date;
|
const value = props.row.original.due_date;
|
||||||
return value
|
return value
|
||||||
@@ -326,6 +328,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'due_status',
|
id: 'due_status',
|
||||||
header: 'Status Jatuh Tempo',
|
header: 'Status Jatuh Tempo',
|
||||||
accessorKey: 'due_status',
|
accessorKey: 'due_status',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.due_status;
|
const value = props.row.original.due_status;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -333,8 +336,9 @@ const DebtSupplierTab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'total_price',
|
id: 'total_price',
|
||||||
header: 'Total Harga',
|
header: 'Nominal Pembelian',
|
||||||
accessorKey: 'total_price',
|
accessorKey: 'total_price',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.total_price;
|
const value = props.row.original.total_price;
|
||||||
return (
|
return (
|
||||||
@@ -354,8 +358,9 @@ const DebtSupplierTab = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'payment_price',
|
id: 'payment_price',
|
||||||
header: 'Harga Pembayaran',
|
header: 'Pembayaran',
|
||||||
accessorKey: 'payment_price',
|
accessorKey: 'payment_price',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.payment_price;
|
const value = props.row.original.payment_price;
|
||||||
return (
|
return (
|
||||||
@@ -374,11 +379,12 @@ const DebtSupplierTab = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'debt_price',
|
id: 'balance',
|
||||||
header: 'Harga Hutang',
|
header: 'Sisa Saldo Hutang',
|
||||||
accessorKey: 'debt_price',
|
accessorKey: 'balance',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.debt_price;
|
const value = props.row.original.balance;
|
||||||
return (
|
return (
|
||||||
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
<div className={`text-right ${value < 0 ? 'text-red-500' : ''}`}>
|
||||||
{formatCurrency(value)}
|
{formatCurrency(value)}
|
||||||
@@ -398,6 +404,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'status',
|
id: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.status;
|
const value = props.row.original.status;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -407,6 +414,7 @@ const DebtSupplierTab = () => {
|
|||||||
id: 'travel_number',
|
id: 'travel_number',
|
||||||
header: 'Nomor Perjalanan',
|
header: 'Nomor Perjalanan',
|
||||||
accessorKey: 'travel_number',
|
accessorKey: 'travel_number',
|
||||||
|
enableSorting: false,
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const value = props.row.original.travel_number;
|
const value = props.row.original.travel_number;
|
||||||
return value || '-';
|
return value || '-';
|
||||||
@@ -421,10 +429,11 @@ const DebtSupplierTab = () => {
|
|||||||
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
className={{ wrapper: 'w-full', body: 'p-1!' }}
|
||||||
>
|
>
|
||||||
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
<div className='mb-4 flex justify-end gap-2 [&_button]:px-4'>
|
||||||
<Button variant='outline' onClick={filterModal.openModal}>
|
<ButtonFilter
|
||||||
<Icon icon='heroicons:funnel' width={18} height={18} />
|
values={formik.values}
|
||||||
Filter
|
onClick={handleFilterModalOpen}
|
||||||
</Button>
|
variant='outline'
|
||||||
|
/>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger={
|
trigger={
|
||||||
@@ -471,9 +480,14 @@ const DebtSupplierTab = () => {
|
|||||||
collapsible={true}
|
collapsible={true}
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
data={supplierReport.rows}
|
data={[
|
||||||
|
{
|
||||||
|
balance: supplierReport.initial_balance,
|
||||||
|
} as DebtRow,
|
||||||
|
...supplierReport.rows,
|
||||||
|
]}
|
||||||
columns={getTableColumns(supplierReport)}
|
columns={getTableColumns(supplierReport)}
|
||||||
pageSize={supplierReport.rows.length}
|
pageSize={supplierReport.rows.length + 1}
|
||||||
renderFooter={supplierReport.rows.length > 0}
|
renderFooter={supplierReport.rows.length > 0}
|
||||||
className={{
|
className={{
|
||||||
containerClassName: 'w-full',
|
containerClassName: 'w-full',
|
||||||
@@ -493,26 +507,38 @@ const DebtSupplierTab = () => {
|
|||||||
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
paginationClassName: 'hidden',
|
paginationClassName: 'hidden',
|
||||||
}}
|
}}
|
||||||
|
renderCustomRow={(row) => {
|
||||||
|
if (row.index == 0) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className='hover:bg-gray-50 transition-colors border-b border-l border-r border-b-gray-200 border-l-gray-200 border-r-gray-200'
|
||||||
|
key={row.index}
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||||
|
colSpan={12}
|
||||||
|
></td>
|
||||||
|
<td className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'>
|
||||||
|
<div
|
||||||
|
className={`text-right ${row.original.balance < 0 ? 'text-red-500' : ''}`}
|
||||||
|
>
|
||||||
|
{formatCurrency(row.original.balance)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className='px-4 py-3 text-xs text-gray-900 whitespace-nowrap'
|
||||||
|
colSpan={2}
|
||||||
|
></td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{meta && data.length > 0 && (
|
|
||||||
<div className='mt-6'>
|
|
||||||
<Pagination
|
|
||||||
currentPage={meta.page}
|
|
||||||
totalItems={meta.total_results}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onRowChange={handleRowChange}
|
|
||||||
onNextPage={handleNextPage}
|
|
||||||
onPrevPage={handlePrevPage}
|
|
||||||
rowOptions={[10, 25, 50, 100]}
|
|
||||||
itemsPerPage={meta.limit}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filter Modal */}
|
{/* Filter Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -522,7 +548,11 @@ const DebtSupplierTab = () => {
|
|||||||
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
|
modalBox: 'p-0 rounded-2xl xl:max-w-4/12 max-w-sm',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='space-y-6'>
|
<form
|
||||||
|
className='space-y-6'
|
||||||
|
onSubmit={formik.handleSubmit}
|
||||||
|
onReset={formik.handleReset}
|
||||||
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
|
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300 px-4'>
|
||||||
<div className='flex items-center gap-2 text-primary'>
|
<div className='flex items-center gap-2 text-primary'>
|
||||||
@@ -542,37 +572,31 @@ const DebtSupplierTab = () => {
|
|||||||
<div>
|
<div>
|
||||||
<DateInput
|
<DateInput
|
||||||
label='Tanggal'
|
label='Tanggal'
|
||||||
name='start_date'
|
name='startDate'
|
||||||
value={filterStartDate}
|
value={formik.values.startDate || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilterStartDate(e.target.value);
|
formik.setFieldValue('startDate', e.target.value || null);
|
||||||
setFilterErrors((prev) => ({ ...prev, start_date: '' }));
|
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.startDate && !!formik.errors.startDate
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.startDate}
|
||||||
/>
|
/>
|
||||||
{filterErrors.start_date && (
|
|
||||||
<p className='text-red-500 text-sm mt-1'>
|
|
||||||
{filterErrors.start_date}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mt-auto'>
|
<div className='mt-auto'>
|
||||||
<DateInput
|
<DateInput
|
||||||
label=' '
|
label=' '
|
||||||
name='end_date'
|
name='endDate'
|
||||||
value={filterEndDate}
|
value={formik.values.endDate || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setFilterEndDate(e.target.value);
|
formik.setFieldValue('endDate', e.target.value || null);
|
||||||
setFilterErrors((prev) => ({ ...prev, end_date: '' }));
|
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={formik.touched.endDate && !!formik.errors.endDate}
|
||||||
|
errorMessage={formik.errors.endDate}
|
||||||
/>
|
/>
|
||||||
{filterErrors.end_date && (
|
|
||||||
<p className='text-red-500 text-sm mt-1'>
|
|
||||||
{filterErrors.end_date}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -582,15 +606,22 @@ const DebtSupplierTab = () => {
|
|||||||
placeholder='Pilih Supplier'
|
placeholder='Pilih Supplier'
|
||||||
isMulti
|
isMulti
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
value={filterSupplier}
|
value={formik.values.supplierIds || []}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setFilterSupplier(
|
formik.setFieldValue(
|
||||||
Array.isArray(val) ? val : val ? [val] : []
|
'supplierIds',
|
||||||
|
Array.isArray(val) ? val : val ? [val] : null
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
isLoading={isLoadingSuppliers}
|
onInputChange={setSupplierInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreSuppliers}
|
||||||
|
isLoading={isLoadingSupplierOptions}
|
||||||
isClearable
|
isClearable
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplierIds && !!formik.errors.supplierIds
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplierIds as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -599,12 +630,17 @@ const DebtSupplierTab = () => {
|
|||||||
label='Filter Berdasarkan'
|
label='Filter Berdasarkan'
|
||||||
placeholder='Pilih Filter Berdasarkan'
|
placeholder='Pilih Filter Berdasarkan'
|
||||||
options={dataTypeOptions}
|
options={dataTypeOptions}
|
||||||
value={filterDateType}
|
value={formik.values.filterBy || null}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setFilterDateType(val ? (val as OptionType) : undefined);
|
formik.setFieldValue(
|
||||||
|
'filterBy',
|
||||||
|
val ? (val as OptionType) : null
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
isClearable
|
isClearable
|
||||||
|
isError={formik.touched.filterBy && !!formik.errors.filterBy}
|
||||||
|
errorMessage={formik.errors.filterBy as string}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -614,18 +650,15 @@ const DebtSupplierTab = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='ms-4 min-w-36 rounded-lg'
|
className='ms-4 min-w-36 rounded-lg'
|
||||||
onClick={handleResetFilters}
|
type='reset'
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button className='me-4 min-w-36 rounded-lg' type='submit'>
|
||||||
className='me-4 min-w-36 rounded-lg'
|
|
||||||
onClick={handleApplyFilters}
|
|
||||||
>
|
|
||||||
Apply Filter
|
Apply Filter
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setAreaInputValue,
|
setInputValue: setAreaInputValue,
|
||||||
options: areaOptions,
|
options: areaOptions,
|
||||||
isLoadingOptions: isLoadingAreaOptions,
|
isLoadingOptions: isLoadingAreaOptions,
|
||||||
|
loadMore: loadMoreAreas,
|
||||||
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
} = useSelect<BaseKandang>(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const areaChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
@@ -78,6 +79,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setLocationInputValue,
|
setInputValue: setLocationInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocationOptions,
|
isLoadingOptions: isLoadingLocationOptions,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<BaseKandang>(LocationApi.basePath, 'id', 'name', 'search', {
|
||||||
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
area_id: selectedArea ? ((selectedArea as OptionType).value as string) : '',
|
||||||
});
|
});
|
||||||
@@ -94,6 +96,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setProjectFlockInputValue,
|
setInputValue: setProjectFlockInputValue,
|
||||||
options: projectFlockOptions,
|
options: projectFlockOptions,
|
||||||
isLoadingOptions: isLoadingProjectFlockOptions,
|
isLoadingOptions: isLoadingProjectFlockOptions,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
} = useSelect<BaseKandang>(
|
} = useSelect<BaseKandang>(
|
||||||
ProjectFlockApi.basePath,
|
ProjectFlockApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -120,6 +123,7 @@ const ProductionResultContent = () => {
|
|||||||
setInputValue: setProjectFlockKandangInputValue,
|
setInputValue: setProjectFlockKandangInputValue,
|
||||||
options: projectFlockKandangOptions,
|
options: projectFlockKandangOptions,
|
||||||
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
isLoadingOptions: isLoadingProjectFlockKandangOptions,
|
||||||
|
loadMore: loadMoreProjectFlockKandangs,
|
||||||
} = useSelect<BaseKandang>(
|
} = useSelect<BaseKandang>(
|
||||||
ProjectFlockKandangApi.basePath,
|
ProjectFlockKandangApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
@@ -235,6 +239,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={areaChangeHandler}
|
onChange={areaChangeHandler}
|
||||||
onInputChange={setAreaInputValue}
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-6 lg:col-span-4',
|
||||||
@@ -251,6 +256,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedArea}
|
isDisabled={!selectedArea}
|
||||||
className={{
|
className={{
|
||||||
@@ -270,6 +276,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedProjectFlock}
|
value={selectedProjectFlock}
|
||||||
onChange={projectFlockChangeHandler}
|
onChange={projectFlockChangeHandler}
|
||||||
onInputChange={setProjectFlockInputValue}
|
onInputChange={setProjectFlockInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedArea || !selectedLocation}
|
isDisabled={!selectedArea || !selectedLocation}
|
||||||
className={{
|
className={{
|
||||||
@@ -289,6 +296,7 @@ const ProductionResultContent = () => {
|
|||||||
value={selectedProjectFlockKandang}
|
value={selectedProjectFlockKandang}
|
||||||
onChange={projectFlockKandangChangeHandler}
|
onChange={projectFlockKandangChangeHandler}
|
||||||
onInputChange={setProjectFlockKandangInputValue}
|
onInputChange={setProjectFlockKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlockKandangs}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!selectedProjectFlock}
|
isDisabled={!selectedProjectFlock}
|
||||||
className={{
|
className={{
|
||||||
|
|||||||
@@ -58,18 +58,26 @@ const HppPerKandangTab = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { options: areaOptions, isLoadingOptions: isLoadingAreas } = useSelect(
|
const {
|
||||||
AreaApi.basePath,
|
setInputValue: setAreaInputValue,
|
||||||
'id',
|
options: areaOptions,
|
||||||
'name',
|
isLoadingOptions: isLoadingAreas,
|
||||||
'search'
|
loadMore: loadMoreAreas,
|
||||||
);
|
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
const { options: locationOptions, isLoadingOptions: isLoadingLocations } =
|
const {
|
||||||
useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
setInputValue: setLocationInputValue,
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocations,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
const { options: kandangOptions, isLoadingOptions: isLoadingKandangs } =
|
const {
|
||||||
useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
setInputValue: setKandangInputValue,
|
||||||
|
options: kandangOptions,
|
||||||
|
isLoadingOptions: isLoadingKandangs,
|
||||||
|
loadMore: loadMoreKandangs,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
const showUnrecordedOptions: OptionType[] = [
|
const showUnrecordedOptions: OptionType[] = [
|
||||||
{ value: 'false', label: 'Sembunyikan' },
|
{ value: 'false', label: 'Sembunyikan' },
|
||||||
@@ -810,6 +818,8 @@ const HppPerKandangTab = () => {
|
|||||||
.includes(String(opt.value))
|
.includes(String(opt.value))
|
||||||
)}
|
)}
|
||||||
onChange={areaChangeHandler}
|
onChange={areaChangeHandler}
|
||||||
|
onInputChange={setAreaInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreAreas}
|
||||||
isLoading={isLoadingAreas}
|
isLoading={isLoadingAreas}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
@@ -824,6 +834,8 @@ const HppPerKandangTab = () => {
|
|||||||
.includes(String(opt.value))
|
.includes(String(opt.value))
|
||||||
)}
|
)}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
|
onInputChange={setLocationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
@@ -838,6 +850,8 @@ const HppPerKandangTab = () => {
|
|||||||
.includes(String(opt.value))
|
.includes(String(opt.value))
|
||||||
)}
|
)}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
|
onInputChange={setKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandangs}
|
||||||
isLoading={isLoadingKandangs}
|
isLoading={isLoadingKandangs}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -457,3 +457,14 @@ export const MARKETING_TYPE_OPTIONS = [
|
|||||||
value: 'trading',
|
value: 'trading',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MARKETING_DATE_FILTER_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: 'Tanggal Realisasi',
|
||||||
|
value: 'realization_date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tanggal SO',
|
||||||
|
value: 'so_date',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ export const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
|||||||
'/dashboard/': ['lti.dashboard.list'],
|
'/dashboard/': ['lti.dashboard.list'],
|
||||||
|
|
||||||
// Daily Checklist
|
// Daily Checklist
|
||||||
// TODO: use real daily checklist permission name
|
'/daily-checklist/dashboard/': ['lti.daily_checklist.dashboard.list'],
|
||||||
// '/daily-checklist/': ['lti.daily_checklist.list'],
|
'/daily-checklist/daily-checklist/': ['lti.daily_checklist.create'],
|
||||||
// '/daily-checklist/dashboard/': ['lti.daily_checklist.list'],
|
'/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
|
||||||
// '/daily-checklist/list-daily-checklist/': ['lti.daily_checklist.list'],
|
'/daily-checklist/list-daily-checklist/detail/': [
|
||||||
// '/daily-checklist/list-daily-checklist/detail/': ['lti.daily_checklist.detail'],
|
'lti.daily_checklist.detail',
|
||||||
// '/daily-checklist/reports/': ['lti.daily_checklist.reports'],
|
],
|
||||||
// '/daily-checklist/master-data/employee/': ['lti.dashboard.master_data.employee'],
|
'/daily-checklist/reports/': ['lti.daily_checklist.reports'],
|
||||||
// '/daily-checklist/master-data/activity/': ['lti.dashboard.master_data.activity'],
|
'/daily-checklist/master-data/employee/': [
|
||||||
'/daily-checklist/dashboard/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.employee',
|
||||||
'/daily-checklist/daily-checklist/': ['lti.dashboard.list'],
|
],
|
||||||
'/daily-checklist/list-daily-checklist/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/activity/': [
|
||||||
'/daily-checklist/list-daily-checklist/detail/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.activity',
|
||||||
'/daily-checklist/reports/': ['lti.dashboard.list'],
|
],
|
||||||
'/daily-checklist/master-data/employee/': ['lti.dashboard.list'],
|
'/daily-checklist/master-data/configuration/': [
|
||||||
'/daily-checklist/master-data/activity/': ['lti.dashboard.list'],
|
'lti.daily_checklist.master_data.configuration',
|
||||||
'/daily-checklist/master-data/configuration/': ['lti.dashboard.list'],
|
],
|
||||||
|
|
||||||
// Production
|
// Production
|
||||||
// Production - Project Flock
|
// Production - Project Flock
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors, FormikValues } from 'formik';
|
||||||
|
|
||||||
export type ErrorMessage = {
|
export type ErrorMessage = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -69,3 +69,66 @@ export function getUniqueFormikErrors<T>(errors: FormikErrors<T>): string[] {
|
|||||||
export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] {
|
export function getAllFormikErrors<T>(errors: FormikErrors<T>): ErrorMessage[] {
|
||||||
return parseFormikErrors(errors);
|
return parseFormikErrors(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is considered "filled" (not empty)
|
||||||
|
* @param value - Value to check
|
||||||
|
* @returns True if value is filled, false otherwise
|
||||||
|
*/
|
||||||
|
function isValueFilled(value: unknown): boolean {
|
||||||
|
// Check for null or undefined
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty string
|
||||||
|
if (typeof value === 'string' && value.trim() === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty array
|
||||||
|
if (Array.isArray(value) && value.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty object (but not Date or other special objects)
|
||||||
|
if (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
!(value instanceof Date) &&
|
||||||
|
Object.keys(value).length === 0
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count the number of filled (non-empty) values in Formik values object
|
||||||
|
* @param values - Formik values object
|
||||||
|
* @returns Number of filled values
|
||||||
|
* @example
|
||||||
|
* const values = {
|
||||||
|
* name: 'John',
|
||||||
|
* email: '',
|
||||||
|
* age: null,
|
||||||
|
* tags: ['tag1', 'tag2'],
|
||||||
|
* emptyArray: [],
|
||||||
|
* };
|
||||||
|
* getFilledFormikValuesCount(values); // Returns 2 (name and tags)
|
||||||
|
*/
|
||||||
|
export function getFilledFormikValuesCount<T extends FormikValues>(
|
||||||
|
values: T
|
||||||
|
): number {
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
Object.keys(values).forEach((key) => {
|
||||||
|
const value = values[key];
|
||||||
|
if (isValueFilled(value)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,10 +131,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getOverhead(
|
async getOverhead(
|
||||||
id: number
|
id: number,
|
||||||
|
kandangId?: number
|
||||||
): Promise<BaseApiResponse<ClosingOverhead> | undefined> {
|
): Promise<BaseApiResponse<ClosingOverhead> | undefined> {
|
||||||
try {
|
try {
|
||||||
const path = `${this.basePath}/${id}/overhead`;
|
const path = `${this.basePath}/${id}${kandangId ? `/${kandangId}` : ''}/overhead`;
|
||||||
return await httpClient<BaseApiResponse<ClosingOverhead>>(path, {
|
return await httpClient<BaseApiResponse<ClosingOverhead>>(path, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
@@ -147,10 +148,11 @@ export class ClosingApiService extends BaseApiService<Closing, null, null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFinance(
|
async getFinance(
|
||||||
id: number
|
id: number,
|
||||||
|
kandangId?: number
|
||||||
): Promise<BaseApiResponse<ClosingFinance> | undefined> {
|
): Promise<BaseApiResponse<ClosingFinance> | undefined> {
|
||||||
try {
|
try {
|
||||||
const path = `${this.basePath}/${id}/keuangan`;
|
const path = `${this.basePath}/${id}${kandangId ? `/${kandangId}` : ''}/keuangan`;
|
||||||
return await httpClient<BaseApiResponse<ClosingFinance>>(path, {
|
return await httpClient<BaseApiResponse<ClosingFinance>>(path, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ export class DebtSupplierApiService extends BaseApiService<
|
|||||||
supplier_ids?: string,
|
supplier_ids?: string,
|
||||||
filter_by?: string,
|
filter_by?: string,
|
||||||
start_date?: string,
|
start_date?: string,
|
||||||
end_date?: string,
|
end_date?: string
|
||||||
page?: number,
|
|
||||||
limit?: number
|
|
||||||
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
|
): Promise<BaseApiResponse<DebtSupplier[]> | undefined> {
|
||||||
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
|
return await this.customRequest<BaseApiResponse<DebtSupplier[]>>(
|
||||||
`debt-supplier`,
|
`debt-supplier`,
|
||||||
@@ -28,8 +26,6 @@ export class DebtSupplierApiService extends BaseApiService<
|
|||||||
filter_by: filter_by,
|
filter_by: filter_by,
|
||||||
start_date: start_date,
|
start_date: start_date,
|
||||||
end_date: end_date,
|
end_date: end_date,
|
||||||
page: page,
|
|
||||||
limit: limit,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ export class MarketingSaleReportService extends BaseApiService<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SaleReportApi = new MarketingSaleReportService(
|
export const SaleReportApi = new MarketingSaleReportService('reports');
|
||||||
'reports/marketings'
|
|
||||||
);
|
|
||||||
|
|
||||||
// export const SaleReportApi = new MarketingSaleReportService(
|
// export const SaleReportApi = new MarketingSaleReportService(
|
||||||
// 'http://localhost:4010/api/reports/marketings'
|
// 'http://localhost:4010/api/reports/marketings'
|
||||||
|
|||||||
+12
-9
@@ -1,20 +1,20 @@
|
|||||||
import { BaseMetadata } from '@/types/api/api-general';
|
import { BaseMetadata } from '@/types/api/api-general';
|
||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { Supplier } from '@/types/api/master-data/supplier';
|
import { BaseSupplier, Supplier } from '@/types/api/master-data/supplier';
|
||||||
|
|
||||||
export type BaseProduct = {
|
export type BaseProduct = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
product_price: number;
|
product_price: number;
|
||||||
selling_price?: number;
|
selling_price?: number;
|
||||||
tax?: number;
|
tax?: number;
|
||||||
expiry_period: number;
|
expiry_period?: number;
|
||||||
uom: Uom;
|
uom: Uom;
|
||||||
product_category: ProductCategory;
|
product_category: ProductCategory;
|
||||||
suppliers: Supplier[];
|
suppliers: (BaseSupplier & { price: number })[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,14 +23,17 @@ export type Product = BaseMetadata & BaseProduct;
|
|||||||
export type CreateProductPayload = {
|
export type CreateProductPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
sku: string;
|
sku?: string;
|
||||||
uom_id: number;
|
uom_id: number;
|
||||||
product_category_id: number;
|
product_category_id: number;
|
||||||
product_price: number;
|
product_price: number;
|
||||||
selling_price: number;
|
selling_price?: number;
|
||||||
tax: number;
|
tax?: number;
|
||||||
expiry_period: number;
|
expiry_period?: number;
|
||||||
supplier_ids: number[];
|
suppliers: {
|
||||||
|
supplier_id: number;
|
||||||
|
price: number;
|
||||||
|
}[];
|
||||||
flags: string[];
|
flags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+33
-12
@@ -1,34 +1,52 @@
|
|||||||
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
|
import { BaseApproval, BaseMetadata, User } from '@/types/api/api-general';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
|
|
||||||
|
export type ProductionStandard = {
|
||||||
|
id: number;
|
||||||
|
week: number;
|
||||||
|
name: string;
|
||||||
|
hen_day_std: number;
|
||||||
|
hen_house_std: number;
|
||||||
|
feed_intake_std: number;
|
||||||
|
max_depletion_std: number;
|
||||||
|
egg_mass_std: number;
|
||||||
|
egg_weight_std: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FCR = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
fcr_std: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProjectFlock = {
|
||||||
|
project_flock_kandang_id: number;
|
||||||
|
flock_name: string;
|
||||||
|
project_flock_category: 'GROWING' | 'LAYING';
|
||||||
|
period: number;
|
||||||
|
production_standart: ProductionStandard;
|
||||||
|
fcr: FCR;
|
||||||
|
total_chick_qty: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProductionMetrics = {
|
export type ProductionMetrics = {
|
||||||
total_depletion_qty: number;
|
total_depletion_qty: number;
|
||||||
cum_depletion_rate: number;
|
cum_depletion_rate: number;
|
||||||
cum_intake: number;
|
cum_intake: number;
|
||||||
fcr_value: number;
|
fcr_value: number;
|
||||||
fcr_std?: number;
|
|
||||||
total_chick_qty: number;
|
|
||||||
hen_day?: number;
|
hen_day?: number;
|
||||||
hen_house?: number;
|
hen_house?: number;
|
||||||
feed_intake?: number;
|
feed_intake?: number;
|
||||||
feed_intake_std?: number;
|
|
||||||
egg_mass?: number;
|
egg_mass?: number;
|
||||||
egg_weight?: number;
|
egg_weight?: number;
|
||||||
hen_day_std?: number;
|
|
||||||
hen_house_std?: number;
|
|
||||||
egg_mass_std?: number;
|
|
||||||
egg_weight_std?: number;
|
|
||||||
daily_gain?: number;
|
|
||||||
avg_daily_gain?: number;
|
|
||||||
cum_depletion?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BaseRecording = {
|
export type BaseRecording = {
|
||||||
id: number;
|
id: number;
|
||||||
project_flock_kandang_id: number;
|
project_flock: ProjectFlock;
|
||||||
record_datetime: string;
|
record_datetime: string;
|
||||||
day: number;
|
day: number;
|
||||||
project_flock_category?: 'GROWING' | 'LAYING';
|
|
||||||
} & ProductionMetrics;
|
} & ProductionMetrics;
|
||||||
|
|
||||||
export type RecordingDepletion = {
|
export type RecordingDepletion = {
|
||||||
@@ -68,6 +86,8 @@ export type Recording = BaseMetadata &
|
|||||||
BaseRecording & {
|
BaseRecording & {
|
||||||
approval?: BaseApproval;
|
approval?: BaseApproval;
|
||||||
created_user: User;
|
created_user: User;
|
||||||
|
warehouse?: Warehouse;
|
||||||
|
product_category?: 'GROWING' | 'LAYING';
|
||||||
depletions?: RecordingDepletion[];
|
depletions?: RecordingDepletion[];
|
||||||
stocks?: RecordingStock[];
|
stocks?: RecordingStock[];
|
||||||
eggs?: RecordingEgg[];
|
eggs?: RecordingEgg[];
|
||||||
@@ -81,6 +101,7 @@ export type NextDayRecording = {
|
|||||||
|
|
||||||
export type CreateGrowingRecordingPayload = {
|
export type CreateGrowingRecordingPayload = {
|
||||||
project_flock_kandang_id: number;
|
project_flock_kandang_id: number;
|
||||||
|
record_date: string;
|
||||||
stocks?: {
|
stocks?: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id: number;
|
||||||
qty: number;
|
qty: number;
|
||||||
|
|||||||
+8
@@ -33,3 +33,11 @@ export interface DebtRow {
|
|||||||
travel_number: string;
|
travel_number: string;
|
||||||
balance: number;
|
balance: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter Param
|
||||||
|
export interface DebtSupplierFilter {
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
supplier_ids?: string;
|
||||||
|
filter_by?: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user