mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
Merge branch 'staging' into 'production'
Staging See merge request mbugroup/lti-web-client!214
This commit is contained in:
Generated
+7
@@ -17,6 +17,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
@@ -7380,6 +7381,12 @@
|
|||||||
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
"integrity": "sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-image": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/html2canvas": {
|
"node_modules/html2canvas": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jspdf": "^3.0.4",
|
"jspdf": "^3.0.4",
|
||||||
"jspdf-autotable": "^5.0.2",
|
"jspdf-autotable": "^5.0.2",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ const FinanceDetailPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(finance);
|
|
||||||
|
|
||||||
// if (!finance || isResponseError(finance)) {
|
// if (!finance || isResponseError(finance)) {
|
||||||
// router.replace('/404');
|
// router.replace('/404');
|
||||||
// return;
|
// return;
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -113,7 +113,15 @@ const DateInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectSingle = (selectedDate?: Date) => {
|
const handleSelectSingle = (selectedDate?: Date) => {
|
||||||
if (!selectedDate) return;
|
if (!selectedDate) {
|
||||||
|
setSelected(undefined);
|
||||||
|
setDisplayValue('');
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: { name, value: '' },
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange?.(syntheticEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (minDate && selectedDate < minDate) {
|
if (minDate && selectedDate < minDate) {
|
||||||
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
setInternalError(`Tanggal tidak boleh sebelum ${min}`);
|
||||||
return;
|
return;
|
||||||
@@ -136,7 +144,15 @@ const DateInput = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
|
const handleSelectRange = (range?: { from?: Date; to?: Date }) => {
|
||||||
if (!range) return;
|
if (!range) {
|
||||||
|
setSelectedRange({});
|
||||||
|
setDisplayValue('');
|
||||||
|
const syntheticEvent = {
|
||||||
|
target: { name, value: { from: '', to: '' } },
|
||||||
|
} as unknown as React.ChangeEvent<HTMLInputElement>;
|
||||||
|
onChange?.(syntheticEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedRange(range);
|
setSelectedRange(range);
|
||||||
|
|
||||||
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
|
const fromStr = range.from ? formatDate(range.from, 'DD/MM/YYYY') : '';
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -3,224 +3,82 @@ import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
|
|||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
import { formatCurrency, formatTitleCase } from '@/lib/helper';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import {
|
import { HppItem, ProfitLossItem } from '@/types/api/closing';
|
||||||
DataSummarySubTotal,
|
import { useSearchParams } from 'next/navigation';
|
||||||
HppPurchaseData,
|
import { useMemo } from 'react';
|
||||||
ProfitLossDataAmount,
|
|
||||||
} from '@/types/api/closing';
|
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
type HppTableRow =
|
|
||||||
| (HppPurchaseData & {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader?: boolean;
|
|
||||||
})
|
|
||||||
| {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: true;
|
|
||||||
type?: never;
|
|
||||||
budgeting?: never;
|
|
||||||
realization?: never;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: string;
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: false;
|
|
||||||
budgeting?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
|
||||||
realization?: { rp_per_bird: number; rp_per_kg: number; amount: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
type ProfitLossTableRow =
|
|
||||||
| (DataSummarySubTotal & {
|
|
||||||
type: string;
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader?: boolean;
|
|
||||||
})
|
|
||||||
| {
|
|
||||||
group_name: string;
|
|
||||||
group_index: number;
|
|
||||||
isGroupHeader: true;
|
|
||||||
type?: never;
|
|
||||||
rp_per_bird?: never;
|
|
||||||
rp_per_kg?: never;
|
|
||||||
amount?: never;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClosingFinanceTable = ({
|
const ClosingFinanceTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: {
|
}: {
|
||||||
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 hppTableData: HppItem[] = useMemo(() => {
|
||||||
group_name: string;
|
if (isResponseSuccess(finance)) {
|
||||||
type: string;
|
const customItems = {
|
||||||
group_index: number;
|
label: 'HPP dan Pengeluaran',
|
||||||
}> = [
|
code: 'custom_row',
|
||||||
{
|
} as HppItem;
|
||||||
group_name: 'HPP dan Pengeluaran',
|
const purchases = finance.data.hpp.items.filter(
|
||||||
type: 'Pembelian PAKAN',
|
(item) => item.category === 'purchase'
|
||||||
group_index: 0,
|
);
|
||||||
},
|
const totalBudgeting = {
|
||||||
{
|
label: 'HPP dan Bahan Baku',
|
||||||
group_name: 'HPP dan Pengeluaran',
|
code: 'custom_row',
|
||||||
type: 'Pembelian STARTER',
|
} as HppItem;
|
||||||
group_index: 0,
|
const overheads = finance.data.hpp.items.filter(
|
||||||
},
|
(item) => item.category === 'overhead'
|
||||||
{
|
);
|
||||||
group_name: 'HPP dan Pengeluaran',
|
return [customItems, ...purchases, totalBudgeting, ...overheads];
|
||||||
type: 'Pembelian DOC',
|
}
|
||||||
group_index: 0,
|
return [];
|
||||||
},
|
}, [finance]);
|
||||||
{
|
|
||||||
group_name: 'HPP dan Pengeluaran',
|
|
||||||
type: 'Pembelian PULLET',
|
|
||||||
group_index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Pengeluaran',
|
|
||||||
type: 'Pembelian LAYER',
|
|
||||||
group_index: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
type: 'Pengeluaran Overhead',
|
|
||||||
group_index: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
type: 'Beban Ekspedisi',
|
|
||||||
group_index: 1,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const hppTableData: HppTableRow[] = [
|
const profitLossTableData: ProfitLossItem[] = useMemo(() => {
|
||||||
{
|
if (isResponseSuccess(finance)) {
|
||||||
group_name: 'HPP dan Pengeluaran',
|
const incomes = finance.data.profit_loss.items.filter(
|
||||||
group_index: 0,
|
(item) => item.type === 'income'
|
||||||
isGroupHeader: true as const,
|
);
|
||||||
},
|
const purchases = finance.data.profit_loss.items.filter(
|
||||||
...staticHppRows
|
(item) => item.type === 'purchase'
|
||||||
.filter((row) => row.group_index === 0)
|
);
|
||||||
.map((staticRow) => {
|
const overheads = finance.data.profit_loss.items.filter(
|
||||||
const apiData = isResponseSuccess(finance)
|
(item) => item.type === 'overhead'
|
||||||
? finance.data.hpp_purchases.hpp
|
);
|
||||||
.find((g) => g.group_name === staticRow.group_name)
|
const grossProfit = {
|
||||||
?.data.find((d) => d.type === staticRow.type)
|
label: 'LABA RUGI BRUTO',
|
||||||
: null;
|
code: 'custom_row',
|
||||||
|
type: 'gross_profit',
|
||||||
return {
|
rp_per_bird:
|
||||||
group_name: staticRow.group_name,
|
finance.data.profit_loss.summary.gross_profit.rp_per_bird ?? 0,
|
||||||
group_index: staticRow.group_index,
|
rp_per_kg: finance.data.profit_loss.summary.gross_profit.rp_per_kg ?? 0,
|
||||||
type: staticRow.type,
|
amount: finance.data.profit_loss.summary.gross_profit.amount ?? 0,
|
||||||
budgeting: apiData?.budgeting || {
|
} as ProfitLossItem;
|
||||||
rp_per_bird: 0,
|
const subtotal = {
|
||||||
rp_per_kg: 0,
|
label: 'Subtotal',
|
||||||
amount: 0,
|
code: 'custom_row',
|
||||||
},
|
type: 'subtotal',
|
||||||
realization: apiData?.realization || {
|
rp_per_bird:
|
||||||
rp_per_bird: 0,
|
finance.data.profit_loss.summary.sub_total.rp_per_bird ?? 0,
|
||||||
rp_per_kg: 0,
|
rp_per_kg: finance.data.profit_loss.summary.sub_total.rp_per_kg ?? 0,
|
||||||
amount: 0,
|
amount: finance.data.profit_loss.summary.sub_total.amount ?? 0,
|
||||||
},
|
} as ProfitLossItem;
|
||||||
isGroupHeader: false as const,
|
return [...incomes, ...purchases, grossProfit, ...overheads, subtotal];
|
||||||
};
|
}
|
||||||
}),
|
return [];
|
||||||
{
|
}, [finance]);
|
||||||
group_name: 'HPP dan Bahan Baku',
|
|
||||||
group_index: 1,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
},
|
|
||||||
...staticHppRows
|
|
||||||
.filter((row) => row.group_index === 1)
|
|
||||||
.map((staticRow) => {
|
|
||||||
const apiData = isResponseSuccess(finance)
|
|
||||||
? finance.data.hpp_purchases.hpp
|
|
||||||
.find((g) => g.group_name === staticRow.group_name)
|
|
||||||
?.data.find((d) => d.type === staticRow.type)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
group_name: staticRow.group_name,
|
|
||||||
group_index: staticRow.group_index,
|
|
||||||
type: staticRow.type,
|
|
||||||
budgeting: apiData?.budgeting || {
|
|
||||||
rp_per_bird: 0,
|
|
||||||
rp_per_kg: 0,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
realization: apiData?.realization || {
|
|
||||||
rp_per_bird: 0,
|
|
||||||
rp_per_kg: 0,
|
|
||||||
amount: 0,
|
|
||||||
},
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
group_name: 'HPP',
|
|
||||||
group_index: 2,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const profitLossTableData: ProfitLossTableRow[] = isResponseSuccess(finance)
|
|
||||||
? [
|
|
||||||
// Pembelian group
|
|
||||||
...finance.data.profit_loss.data.pembelian.map((item) => ({
|
|
||||||
label: 'Pembelian',
|
|
||||||
group_name: 'Pembelian',
|
|
||||||
group_index: 1,
|
|
||||||
type: item.type,
|
|
||||||
rp_per_bird: item.rp_per_bird,
|
|
||||||
rp_per_kg: item.rp_per_kg,
|
|
||||||
amount: item.amount,
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
label: finance.data.profit_loss.data.summary.gross_profit.label,
|
|
||||||
group_name: 'Penjualan',
|
|
||||||
group_index: 0,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
type: finance.data.profit_loss.data.summary.gross_profit.label,
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_bird,
|
|
||||||
rp_per_kg:
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit.rp_per_kg,
|
|
||||||
amount: finance.data.profit_loss.data.summary.gross_profit.amount,
|
|
||||||
},
|
|
||||||
// Penjualan group
|
|
||||||
...finance.data.profit_loss.data.penjualan.map((item) => ({
|
|
||||||
label: 'Penjualan',
|
|
||||||
group_name: 'Penjualan',
|
|
||||||
group_index: 0,
|
|
||||||
type: item.type,
|
|
||||||
rp_per_bird: item.rp_per_bird,
|
|
||||||
rp_per_kg: item.rp_per_kg,
|
|
||||||
amount: item.amount,
|
|
||||||
isGroupHeader: false as const,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
label: finance.data.profit_loss.data.summary.sub_total.label,
|
|
||||||
group_name: 'Pembelian',
|
|
||||||
group_index: 1,
|
|
||||||
isGroupHeader: true as const,
|
|
||||||
type: finance.data.profit_loss.data.summary.sub_total.label,
|
|
||||||
rp_per_bird:
|
|
||||||
finance.data.profit_loss.data.summary.sub_total.rp_per_bird,
|
|
||||||
rp_per_kg: finance.data.profit_loss.data.summary.sub_total.rp_per_kg,
|
|
||||||
amount: finance.data.profit_loss.data.summary.sub_total.amount,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-4'>
|
<div className='flex flex-col gap-4'>
|
||||||
@@ -233,35 +91,21 @@ const ClosingFinanceTable = ({
|
|||||||
>
|
>
|
||||||
<div className='grid grid-cols-2 gap-6'>
|
<div className='grid grid-cols-2 gap-6'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div>
|
<div>Laba Rugi Brutto</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.gross_profit
|
|
||||||
.label || '-'
|
|
||||||
)
|
|
||||||
: 'Laba Rugi Brutto'}
|
|
||||||
</div>
|
|
||||||
<div className='text-lg font-bold'>
|
<div className='text-lg font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.gross_profit.amount
|
finance.data.profit_loss.summary.gross_profit.amount
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div>
|
<div>Laba Rugi Netto</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.net_profit.label ||
|
|
||||||
'-'
|
|
||||||
)
|
|
||||||
: 'Laba Rugi Netto'}
|
|
||||||
</div>
|
|
||||||
<div className='text-lg font-bold'>
|
<div className='text-lg font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit.amount
|
finance.data.profit_loss.summary.net_profit.amount
|
||||||
)
|
)
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
@@ -269,11 +113,7 @@ const ClosingFinanceTable = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title='HPP Purchases'
|
||||||
isResponseSuccess(finance)
|
|
||||||
? finance.data.hpp_purchases.title
|
|
||||||
: 'HPP Purchases'
|
|
||||||
}
|
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible
|
collapsible
|
||||||
className={{
|
className={{
|
||||||
@@ -281,17 +121,18 @@ const ClosingFinanceTable = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<HppTableRow>
|
<Table<HppItem>
|
||||||
data={hppTableData}
|
data={hppTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'No.',
|
header: 'No.',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item, index) => {
|
accessorFn: (item, index) => {
|
||||||
if (item.isGroupHeader) return '-';
|
if (item.code === 'custom_row') return '-';
|
||||||
const dataRowsBefore = hppTableData
|
const dataRowsBefore = hppTableData
|
||||||
.slice(0, index)
|
.slice(0, index)
|
||||||
.filter((row) => !row.isGroupHeader).length;
|
.filter((row) => row.code !== 'custom_row').length;
|
||||||
return dataRowsBefore + 1;
|
return dataRowsBefore + 1;
|
||||||
},
|
},
|
||||||
footer: (props) => {
|
footer: (props) => {
|
||||||
@@ -299,9 +140,9 @@ const ClosingFinanceTable = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Type',
|
header: 'Jenis',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatTitleCase(item.type || '-'),
|
accessorFn: (item) => formatTitleCase(item.label || '-'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Budgeting',
|
header: 'Budgeting',
|
||||||
@@ -317,7 +158,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_rp_per_bird' &&
|
return props.column.id === 'budgeting_rp_per_bird' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting
|
||||||
?.rp_per_bird || 0
|
?.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
@@ -333,8 +174,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_rp_per_kg' &&
|
return props.column.id === 'budgeting_rp_per_kg' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting?.rp_per_kg ||
|
||||||
?.rp_per_kg || 0
|
0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -349,8 +190,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'budgeting_amount' &&
|
return props.column.id === 'budgeting_amount' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp?.budgeting
|
finance.data.hpp.summary?.budgeting?.amount || 0
|
||||||
?.amount || 0
|
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -371,8 +211,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_rp_per_bird' &&
|
return props.column.id === 'realization_rp_per_bird' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization
|
||||||
?.realization?.rp_per_bird || 0
|
?.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -387,8 +227,8 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_rp_per_kg' &&
|
return props.column.id === 'realization_rp_per_kg' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization
|
||||||
?.realization?.rp_per_kg || 0
|
?.rp_per_kg || 0
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -403,8 +243,7 @@ const ClosingFinanceTable = ({
|
|||||||
return props.column.id === 'realization_amount' &&
|
return props.column.id === 'realization_amount' &&
|
||||||
isResponseSuccess(finance)
|
isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.hpp_purchases.summary_hpp
|
finance.data.hpp.summary?.realization?.amount || 0
|
||||||
?.realization?.amount || 0
|
|
||||||
)
|
)
|
||||||
: '-';
|
: '-';
|
||||||
},
|
},
|
||||||
@@ -414,7 +253,7 @@ const ClosingFinanceTable = ({
|
|||||||
]}
|
]}
|
||||||
renderCustomRow={(row) => {
|
renderCustomRow={(row) => {
|
||||||
const rowData = row.original;
|
const rowData = row.original;
|
||||||
if (rowData.isGroupHeader) {
|
if (rowData.code === 'custom_row') {
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
@@ -428,7 +267,7 @@ const ClosingFinanceTable = ({
|
|||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
||||||
>
|
>
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{formatTitleCase(rowData.group_name ?? '-')}
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -441,11 +280,7 @@ const ClosingFinanceTable = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
<Card
|
||||||
title={
|
title='Profit/Loss'
|
||||||
isResponseSuccess(finance)
|
|
||||||
? finance.data.profit_loss.title
|
|
||||||
: 'Profit/Loss'
|
|
||||||
}
|
|
||||||
variant='bordered'
|
variant='bordered'
|
||||||
collapsible
|
collapsible
|
||||||
className={{
|
className={{
|
||||||
@@ -453,38 +288,32 @@ const ClosingFinanceTable = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='mt-6 p-0 mb-0'>
|
<div className='mt-6 p-0 mb-0'>
|
||||||
<Table<ProfitLossTableRow>
|
<Table<ProfitLossItem>
|
||||||
data={profitLossTableData}
|
data={profitLossTableData}
|
||||||
|
isLoading={isLoading}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Jenis',
|
header: 'Jenis',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => item.type,
|
accessorFn: (item) => item.label,
|
||||||
cell: (item) => (
|
cell: (item) => (
|
||||||
<div className=''>
|
<div className=''>
|
||||||
{formatTitleCase(item.row.original.type || '-')}
|
{formatTitleCase(item.row.original.label || '-')}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold uppercase'>
|
<div className='font-bold uppercase'>LABA RUGI NETTO</div>
|
||||||
{isResponseSuccess(finance)
|
|
||||||
? formatTitleCase(
|
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
|
||||||
.label || '-'
|
|
||||||
)
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Rp/Ekor',
|
header: 'Rp/Ekor',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
accessorFn: (item) => formatCurrency(item.rp_per_bird || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.rp_per_bird || 0
|
.rp_per_bird || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -495,11 +324,11 @@ const ClosingFinanceTable = ({
|
|||||||
header: 'Rp/Kg',
|
header: 'Rp/Kg',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
accessorFn: (item) => formatCurrency(item.rp_per_kg || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.rp_per_kg || 0
|
.rp_per_kg || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -510,11 +339,11 @@ const ClosingFinanceTable = ({
|
|||||||
header: 'Jumlah (Rp)',
|
header: 'Jumlah (Rp)',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
accessorFn: (item) => formatCurrency(item.amount || 0),
|
accessorFn: (item) => formatCurrency(item.amount || 0),
|
||||||
footer: (item) => (
|
footer: () => (
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{isResponseSuccess(finance)
|
{isResponseSuccess(finance)
|
||||||
? formatCurrency(
|
? formatCurrency(
|
||||||
finance.data.profit_loss.data.summary.net_profit
|
finance.data.profit_loss.summary.net_profit
|
||||||
.amount || 0
|
.amount || 0
|
||||||
)
|
)
|
||||||
: formatCurrency(0)}
|
: formatCurrency(0)}
|
||||||
@@ -524,55 +353,30 @@ const ClosingFinanceTable = ({
|
|||||||
]}
|
]}
|
||||||
renderCustomRow={(row) => {
|
renderCustomRow={(row) => {
|
||||||
const rowData = row.original;
|
const rowData = row.original;
|
||||||
if (rowData.isGroupHeader) {
|
if (rowData.code === 'custom_row') {
|
||||||
if (rowData.amount) {
|
|
||||||
return (
|
|
||||||
<tr
|
|
||||||
key={row.id}
|
|
||||||
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
|
||||||
>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold ps-6 uppercase'>
|
|
||||||
{formatTitleCase(rowData.label ?? '-')}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
|
||||||
>
|
|
||||||
<div className='font-bold'>
|
|
||||||
{formatCurrency(rowData.amount ?? 0)}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={row.id}
|
key={row.id}
|
||||||
className={TABLE_DEFAULT_STYLING.bodyRowClassName}
|
className={TABLE_DEFAULT_STYLING.footerRowClassName}
|
||||||
>
|
>
|
||||||
<td
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
colSpan={4}
|
<div className='font-bold ps-6 uppercase'>
|
||||||
className={TABLE_DEFAULT_STYLING.bodyColumnClassName}
|
{formatTitleCase(rowData.label ?? '-')}
|
||||||
>
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
<div className='font-bold'>
|
<div className='font-bold'>
|
||||||
{formatTitleCase(rowData.group_name ?? '-')}
|
{formatCurrency(rowData.rp_per_bird ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.rp_per_kg ?? 0)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className={TABLE_DEFAULT_STYLING.bodyColumnClassName}>
|
||||||
|
<div className='font-bold'>
|
||||||
|
{formatCurrency(rowData.amount ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { ClosingIncomingSapronakSummary } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingIncomingSapronaksSummaryTableProps {
|
||||||
|
projectFlockId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingIncomingSapronaksSummaryTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingIncomingSapronaksSummaryTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: incomingSapronakSummaries,
|
||||||
|
isLoading: isLoadingIncomingSapronakSummaries,
|
||||||
|
} = useSWR(
|
||||||
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
|
ClosingApi.getAllIncomingSapronakSummaryFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const incomingSapronaksColumns: ColumnDef<ClosingIncomingSapronakSummary>[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'category',
|
||||||
|
header: 'Kategori',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'total_qty',
|
||||||
|
header: 'Total Kuantitas',
|
||||||
|
cell: (props) =>
|
||||||
|
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
|
? incomingSapronakSummaries.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [incomingSapronakSummaries, isResponseSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Ringkasan Sapronak Masuk</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<Table<ClosingIncomingSapronakSummary>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
|
? incomingSapronakSummaries?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={incomingSapronaksColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
|
? incomingSapronakSummaries?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(incomingSapronakSummaries)
|
||||||
|
? incomingSapronakSummaries?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingIncomingSapronakSummaries}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(incomingSapronakSummaries) &&
|
||||||
|
incomingSapronakSummaries?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingIncomingSapronaksSummaryTable;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingIncomingSapronaksTableProps {
|
|||||||
const ClosingIncomingSapronaksTable = ({
|
const ClosingIncomingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingIncomingSapronaksTableProps) => {
|
}: ClosingIncomingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingIncomingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
const { data: incomingSapronaks, isLoading: isLoadingIncomingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=incoming&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllIncomingSapronakFetcher,
|
ClosingApi.getAllIncomingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
import Collapse from '@/components/Collapse';
|
||||||
|
|
||||||
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
|
import { ClosingOutgoingSapronakSummary } from '@/types/api/closing';
|
||||||
|
|
||||||
|
interface ClosingOutgoingSapronaksSummaryTableProps {
|
||||||
|
projectFlockId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClosingOutgoingSapronaksSummaryTable = ({
|
||||||
|
projectFlockId,
|
||||||
|
}: ClosingOutgoingSapronaksSummaryTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: tableFilterState,
|
||||||
|
updateFilter,
|
||||||
|
setPage,
|
||||||
|
setPageSize,
|
||||||
|
toQueryString: getTableFilterQueryString,
|
||||||
|
} = useTableFilter({
|
||||||
|
initial: {
|
||||||
|
search: '',
|
||||||
|
nameSort: '',
|
||||||
|
},
|
||||||
|
paramMap: {
|
||||||
|
page: 'page',
|
||||||
|
pageSize: 'limit',
|
||||||
|
nameSort: 'sort_name',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: outgoingSapronakSummaries,
|
||||||
|
isLoading: isLoadingOutgoingSapronakSummaries,
|
||||||
|
} = useSWR(
|
||||||
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak/summary${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
|
ClosingApi.getAllIncomingSapronakSummaryFetcher,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const outgoingSapronaksColumns: ColumnDef<ClosingOutgoingSapronakSummary>[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: '#',
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'category',
|
||||||
|
header: 'Kategori',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'total_qty',
|
||||||
|
header: 'Total Kuantitas',
|
||||||
|
cell: (props) =>
|
||||||
|
`${formatNumber(props.row.original.total_qty)} ${props.row.original.uom.name}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
updateFilter('search', e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// track sorting
|
||||||
|
useEffect(() => {
|
||||||
|
const isNameSorted = sorting.find((sortItem) => sortItem.id === 'name');
|
||||||
|
|
||||||
|
if (!isNameSorted) {
|
||||||
|
updateFilter('nameSort', '');
|
||||||
|
} else {
|
||||||
|
updateFilter('nameSort', isNameSorted.desc ? 'desc' : 'asc');
|
||||||
|
}
|
||||||
|
}, [sorting, updateFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setOpen(
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
|
? outgoingSapronakSummaries.data.length > 0
|
||||||
|
: false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [outgoingSapronakSummaries, isResponseSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full',
|
||||||
|
body: 'p-4 shadow',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>Ringkasan Sapronak Keluar</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<div className='w-full p-0'>
|
||||||
|
<Table<ClosingOutgoingSapronakSummary>
|
||||||
|
data={
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
|
? outgoingSapronakSummaries?.data
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
columns={outgoingSapronaksColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
rowOptions={[10, 20, 50, 100]}
|
||||||
|
page={
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
|
? outgoingSapronakSummaries?.meta?.page
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries)
|
||||||
|
? outgoingSapronakSummaries?.meta?.total_results
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoadingOutgoingSapronakSummaries}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'w-full mb-20':
|
||||||
|
isResponseSuccess(outgoingSapronakSummaries) &&
|
||||||
|
outgoingSapronakSummaries?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClosingOutgoingSapronaksSummaryTable;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChangeEventHandler, useEffect, useState } from 'react';
|
import { ChangeEventHandler, useEffect, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
import { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||||
|
|
||||||
@@ -23,6 +24,9 @@ interface ClosingOutgoingSapronaksTableProps {
|
|||||||
const ClosingOutgoingSapronaksTable = ({
|
const ClosingOutgoingSapronaksTable = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingOutgoingSapronaksTableProps) => {
|
}: ClosingOutgoingSapronaksTableProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: tableFilterState,
|
state: tableFilterState,
|
||||||
updateFilter,
|
updateFilter,
|
||||||
@@ -43,7 +47,7 @@ const ClosingOutgoingSapronaksTable = ({
|
|||||||
|
|
||||||
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
const { data: outgoingSapronaks, isLoading: isLoadingOutgoingSapronaks } =
|
||||||
useSWR(
|
useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing`,
|
`${ClosingApi.basePath}/${projectFlockId}/sapronak${getTableFilterQueryString()}&type=outgoing&kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
ClosingApi.getAllOutgoingSapronakFetcher,
|
ClosingApi.getAllOutgoingSapronakFetcher,
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
|||||||
@@ -5,122 +5,187 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper function to create columns with footer support
|
// Helper function to create columns with footer support
|
||||||
const createColumns = (total?: OverheadTotal): ColumnDef<Overhead>[] => [
|
const createColumns = (
|
||||||
// Group untuk kolom tanpa footer
|
total?: OverheadTotal,
|
||||||
{
|
kandangId?: number
|
||||||
header: 'Nama Item',
|
): ColumnDef<Overhead>[] => {
|
||||||
accessorFn: (props) => props.item_name,
|
const flockColumn: ColumnDef<Overhead>[] = [
|
||||||
footer: 'Total Pengeluaran Overhead',
|
{
|
||||||
},
|
header: 'Budget Pengajuan',
|
||||||
{
|
footer: '',
|
||||||
header: 'Satuan',
|
columns: [
|
||||||
accessorFn: (props) => props.uom_name,
|
{
|
||||||
},
|
id: 'budget_quantity',
|
||||||
{
|
header: 'Jumlah',
|
||||||
header: 'Budget Pengajuan',
|
accessorFn: (props) =>
|
||||||
footer: '',
|
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
||||||
columns: [
|
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
||||||
{
|
},
|
||||||
id: 'budget_quantity',
|
{
|
||||||
header: 'Jumlah',
|
id: 'budget_unit_price',
|
||||||
accessorFn: (props) =>
|
header: 'Harga Satuan',
|
||||||
props.budget_quantity ? formatNumber(props.budget_quantity) : '-',
|
accessorFn: (props) =>
|
||||||
footer: total ? () => formatNumber(total.budget_quantity) : '',
|
props.budget_unit_price
|
||||||
},
|
? formatCurrency(props.budget_unit_price)
|
||||||
{
|
: '-',
|
||||||
id: 'budget_unit_price',
|
footer: '',
|
||||||
header: 'Harga Satuan',
|
},
|
||||||
accessorFn: (props) =>
|
{
|
||||||
props.budget_unit_price
|
id: 'budget_total_amount',
|
||||||
? formatCurrency(props.budget_unit_price)
|
header: 'Total',
|
||||||
: '-',
|
accessorFn: (props) =>
|
||||||
footer: '',
|
props.budget_total_amount
|
||||||
},
|
? formatCurrency(props.budget_total_amount)
|
||||||
{
|
: '-',
|
||||||
id: 'budget_total_amount',
|
footer: total
|
||||||
header: 'Total',
|
? () => formatCurrency(total.budget_total_amount)
|
||||||
accessorFn: (props) =>
|
: '',
|
||||||
props.budget_total_amount
|
},
|
||||||
? formatCurrency(props.budget_total_amount)
|
],
|
||||||
: '-',
|
},
|
||||||
footer: total ? () => formatCurrency(total.budget_total_amount) : '',
|
{
|
||||||
},
|
header: 'Realisasi',
|
||||||
],
|
footer: '',
|
||||||
},
|
columns: [
|
||||||
{
|
{
|
||||||
header: 'Realisasi',
|
id: 'actual_date',
|
||||||
footer: '',
|
header: 'Tanggal',
|
||||||
columns: [
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_date
|
||||||
id: 'actual_date',
|
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||||
header: 'Tanggal',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: '',
|
||||||
props.actual_date
|
},
|
||||||
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
{
|
||||||
: '-',
|
id: 'actual_quantity',
|
||||||
footer: '',
|
header: 'Jumlah',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||||
id: 'actual_quantity',
|
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||||
header: 'Jumlah',
|
},
|
||||||
accessorFn: (props) =>
|
{
|
||||||
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
id: 'actual_unit_price',
|
||||||
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
header: 'Harga Satuan',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_unit_price
|
||||||
id: 'actual_unit_price',
|
? formatCurrency(props.actual_unit_price)
|
||||||
header: 'Harga Satuan',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: '',
|
||||||
props.actual_unit_price
|
},
|
||||||
? formatCurrency(props.actual_unit_price)
|
{
|
||||||
: '-',
|
id: 'actual_total_amount',
|
||||||
footer: '',
|
header: 'Total',
|
||||||
},
|
accessorFn: (props) =>
|
||||||
{
|
props.actual_total_amount
|
||||||
id: 'actual_total_amount',
|
? formatCurrency(props.actual_total_amount)
|
||||||
header: 'Total',
|
: '-',
|
||||||
accessorFn: (props) =>
|
footer: total
|
||||||
props.actual_total_amount
|
? () => formatCurrency(total.actual_total_amount)
|
||||||
? formatCurrency(props.actual_total_amount)
|
: '',
|
||||||
: '-',
|
},
|
||||||
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
},
|
|
||||||
{
|
const kandangColumn: ColumnDef<Overhead>[] = [
|
||||||
id: 'cost_per_bird',
|
{
|
||||||
header: 'Rp/Ekor',
|
id: 'actual_date',
|
||||||
accessorFn: (props) =>
|
header: 'Tanggal',
|
||||||
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
accessorFn: (props) =>
|
||||||
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
props.actual_date
|
||||||
},
|
? formatDate(props.actual_date, 'DD MMM, YYYY')
|
||||||
];
|
: '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_quantity',
|
||||||
|
header: 'Jumlah',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_quantity ? formatNumber(props.actual_quantity) : '-',
|
||||||
|
footer: total ? () => formatNumber(total.actual_quantity) : '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_unit_price',
|
||||||
|
header: 'Harga Satuan',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_unit_price
|
||||||
|
? formatCurrency(props.actual_unit_price)
|
||||||
|
: '-',
|
||||||
|
footer: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'actual_total_amount',
|
||||||
|
header: 'Total',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.actual_total_amount
|
||||||
|
? formatCurrency(props.actual_total_amount)
|
||||||
|
: '-',
|
||||||
|
footer: total ? () => formatCurrency(total.actual_total_amount) : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const finalColumns: ColumnDef<Overhead>[] = [
|
||||||
|
// Group untuk kolom tanpa footer
|
||||||
|
{
|
||||||
|
header: 'No',
|
||||||
|
accessorFn: (_, index) => index,
|
||||||
|
cell: (props) => props.row.index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Nama Item',
|
||||||
|
accessorFn: (props) => props.item_name,
|
||||||
|
footer: 'Total Pengeluaran Overhead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Satuan',
|
||||||
|
accessorFn: (props) => props.uom_name,
|
||||||
|
},
|
||||||
|
...(kandangId ? kandangColumn : flockColumn),
|
||||||
|
{
|
||||||
|
id: 'cost_per_bird',
|
||||||
|
header: 'Rp/Ekor',
|
||||||
|
accessorFn: (props) =>
|
||||||
|
props.cost_per_bird ? formatCurrency(props.cost_per_bird) : '-',
|
||||||
|
footer: total ? () => formatCurrency(total.cost_per_bird) : '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return finalColumns;
|
||||||
|
};
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() =>
|
() =>
|
||||||
isResponseSuccess(overhead)
|
isResponseSuccess(overhead)
|
||||||
? createColumns(overhead.data?.total)
|
? createColumns(
|
||||||
|
overhead.data?.total,
|
||||||
|
kandangId ? Number(kandangId) : undefined
|
||||||
|
)
|
||||||
: createColumns(),
|
: createColumns(),
|
||||||
[overhead]
|
[overhead]
|
||||||
);
|
);
|
||||||
@@ -148,6 +213,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
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ClosingApi } from '@/services/api/closing';
|
import { ClosingApi } from '@/services/api/closing';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -12,9 +13,12 @@ interface ClosingProductionDataTabContentProps {
|
|||||||
const ClosingProductionDataTabContent = ({
|
const ClosingProductionDataTabContent = ({
|
||||||
projectFlockId,
|
projectFlockId,
|
||||||
}: ClosingProductionDataTabContentProps) => {
|
}: ClosingProductionDataTabContentProps) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const kandangId = searchParams.get('kandangId');
|
||||||
|
|
||||||
const { data: productionData, isLoading } = useSWR(
|
const { data: productionData, isLoading } = useSWR(
|
||||||
`${ClosingApi.basePath}/${projectFlockId}/production-data`,
|
`${ClosingApi.basePath}/${projectFlockId}/production-data?kandang_id=${kandangId ? `${kandangId}` : ''}`,
|
||||||
() => ClosingApi.getProductionData(projectFlockId)
|
() => ClosingApi.getProductionData(projectFlockId, Number(kandangId))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -197,7 +201,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 +210,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)}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
import ClosingIncomingSapronaksTable from '@/components/pages/closing/ClosingIncomingSapronaksTable';
|
||||||
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
import ClosingOutgoingSapronaksTable from '@/components/pages/closing/ClosingOutgoingSapronaksTable';
|
||||||
|
import ClosingIncomingSapronaksSummaryTable from '@/components/pages/closing/ClosingIncomingSapronaksSummaryTable';
|
||||||
|
import ClosingOutgoingSapronaksSummaryTable from './ClosingOutgoingSapronaksSummaryTable';
|
||||||
|
|
||||||
interface ClosingSapronakTableProps {
|
interface ClosingSapronakTableProps {
|
||||||
projectFlockId?: number;
|
projectFlockId?: number;
|
||||||
@@ -16,7 +18,15 @@ const ClosingSapronakTabContent = ({
|
|||||||
<>
|
<>
|
||||||
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
<ClosingIncomingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
|
||||||
|
<ClosingIncomingSapronaksSummaryTable
|
||||||
|
projectFlockId={projectFlockId}
|
||||||
|
/>
|
||||||
|
|
||||||
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
<ClosingOutgoingSapronaksTable projectFlockId={projectFlockId} />
|
||||||
|
|
||||||
|
<ClosingOutgoingSapronaksSummaryTable
|
||||||
|
projectFlockId={projectFlockId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -82,12 +82,12 @@ const SalesReportTable = ({
|
|||||||
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
<div className='font-semibold text-gray-900'>Total Penjualan</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: 'age',
|
// id: 'age',
|
||||||
accessorKey: 'age',
|
// accessorKey: 'age',
|
||||||
header: 'Umur',
|
// header: 'Umur',
|
||||||
cell: (props) => props.getValue() || '-',
|
// cell: (props) => props.getValue() || '-',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
id: 'do_number',
|
id: 'do_number',
|
||||||
accessorKey: 'do_number',
|
accessorKey: 'do_number',
|
||||||
|
|||||||
@@ -8,19 +8,22 @@ import SelectInput, {
|
|||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } 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';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
import { KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
|
import { generateDashboardPDF } from '@/components/pages/dashboard/export/DashboardPDF';
|
||||||
import {
|
import {
|
||||||
DashboardFilterType,
|
DashboardFilterType,
|
||||||
getDashboardFilterSchema,
|
getDashboardFilterSchema,
|
||||||
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
} from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
import DashboardLineChart from '@/components/pages/dashboard/chart/DashboardLineChart';
|
||||||
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
import DashboardLineChartSkeleton from '@/components/pages/dashboard/skeleton/DashboardLineChartSkeleton';
|
||||||
|
import DashboardAllCharts, {
|
||||||
|
DashboardAllChartsRef,
|
||||||
|
} from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
import { RadioGroup, RadioGroupItem } from '@/components/input/RadioInput';
|
||||||
import {
|
import {
|
||||||
DashboardFilter,
|
DashboardFilter,
|
||||||
@@ -30,6 +33,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 Dropdown from '@/components/Dropdown';
|
||||||
|
import Menu from '@/components/menu/Menu';
|
||||||
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import { useDashboardStore } from '@/stores/dashboard';
|
||||||
|
|
||||||
// Helper function to normalize values to array
|
// Helper function to normalize values to array
|
||||||
const normalizeToArray = (
|
const normalizeToArray = (
|
||||||
@@ -44,11 +52,22 @@ const normalizeToArray = (
|
|||||||
|
|
||||||
const DashboardProduction = () => {
|
const DashboardProduction = () => {
|
||||||
const filterModal = useModal();
|
const filterModal = useModal();
|
||||||
|
|
||||||
|
// ===== DASHBOARD STORE =====
|
||||||
|
const { filterValues, setFilterValues, resetFilterValues } =
|
||||||
|
useDashboardStore();
|
||||||
|
|
||||||
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
const [analysisMode, setAnalysisMode] = useState<'OVERVIEW' | 'COMPARISON'>(
|
||||||
'OVERVIEW'
|
(filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON') || 'OVERVIEW'
|
||||||
);
|
);
|
||||||
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
const [endpointUrl, setEndpointUrl] = useState('/dashboards');
|
||||||
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>([]);
|
const [selectedLocationIds, setSelectedLocationIds] = useState<number[]>(
|
||||||
|
normalizeToArray(filterValues.location)
|
||||||
|
);
|
||||||
|
const [exporting, setExporting] = useState(false);
|
||||||
|
const statsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const allChartsRef = useRef<DashboardAllChartsRef>(null);
|
||||||
|
|
||||||
// ===== FETCH DATA =====
|
// ===== FETCH DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -64,22 +83,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' },
|
||||||
@@ -89,20 +118,21 @@ const DashboardProduction = () => {
|
|||||||
// ===== FORMIK =====
|
// ===== FORMIK =====
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
startDate: '',
|
startDate: filterValues.startDate || '',
|
||||||
endDate: '',
|
endDate: filterValues.endDate || '',
|
||||||
flock: [] as OptionType[],
|
flock: filterValues.flock || ([] as OptionType[]),
|
||||||
location: [] as OptionType[],
|
location: filterValues.location || ([] as OptionType[]),
|
||||||
kandang: [] as OptionType[],
|
kandang: filterValues.kandang || ([] as OptionType[]),
|
||||||
analysisMode: analysisMode,
|
analysisMode: filterValues.analysisMode || analysisMode,
|
||||||
comparisonType: '',
|
comparisonType: filterValues.comparisonType || '',
|
||||||
lokasiIds: [],
|
locationIds: filterValues.locationIds || [],
|
||||||
flockIds: [],
|
flockIds: filterValues.flockIds || [],
|
||||||
kandangIds: [],
|
kandangIds: filterValues.kandangIds || [],
|
||||||
} as DashboardFilterType,
|
} as DashboardFilterType,
|
||||||
validationSchema: getDashboardFilterSchema(analysisMode),
|
validationSchema: getDashboardFilterSchema(analysisMode),
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
console.log(values);
|
// Save filter values to store
|
||||||
|
setFilterValues(values);
|
||||||
|
|
||||||
handleApplyFilter({
|
handleApplyFilter({
|
||||||
start_date: values.startDate || '',
|
start_date: values.startDate || '',
|
||||||
@@ -118,13 +148,13 @@ const DashboardProduction = () => {
|
|||||||
|
|
||||||
const handleResetFilter = () => {
|
const handleResetFilter = () => {
|
||||||
formik.resetForm();
|
formik.resetForm();
|
||||||
|
resetFilterValues(); // Clear stored filter values
|
||||||
setAnalysisMode('OVERVIEW');
|
setAnalysisMode('OVERVIEW');
|
||||||
setEndpointUrl('/dashboards');
|
setEndpointUrl('/dashboards');
|
||||||
|
setSelectedLocationIds([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApplyFilter = (values: DashboardFilter) => {
|
const handleApplyFilter = (values: DashboardFilter) => {
|
||||||
console.log(values);
|
|
||||||
|
|
||||||
// Build query params object, only include non-empty values
|
// Build query params object, only include non-empty values
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
@@ -140,15 +170,37 @@ const DashboardProduction = () => {
|
|||||||
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
if (values.comparison_type) params.comparison_type = values.comparison_type;
|
||||||
|
|
||||||
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
setEndpointUrl(`/dashboards?${new URLSearchParams(params).toString()}`);
|
||||||
console.log(endpointUrl);
|
|
||||||
filterModal.closeModal();
|
filterModal.closeModal();
|
||||||
refreshDashboardProductionData();
|
refreshDashboardProductionData();
|
||||||
formik.resetForm();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ===== Load filter from store on mount =====
|
||||||
|
useEffect(() => {
|
||||||
|
if (!filterValues) return;
|
||||||
|
handleApplyFilter({
|
||||||
|
start_date: filterValues.startDate,
|
||||||
|
end_date: filterValues.endDate,
|
||||||
|
analysis_mode: filterValues.analysisMode as 'OVERVIEW' | 'COMPARISON',
|
||||||
|
location_ids: normalizeToArray(filterValues.location),
|
||||||
|
flock_ids: normalizeToArray(filterValues.flock),
|
||||||
|
kandang_ids: normalizeToArray(filterValues.kandang),
|
||||||
|
comparison_type: filterValues.comparisonType,
|
||||||
|
});
|
||||||
|
}, [filterValues]);
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
||||||
|
|
||||||
|
// ===== Export PDF =====
|
||||||
|
const handleExportPDF = async () => {
|
||||||
|
await generateDashboardPDF({
|
||||||
|
filterValues: formik.values,
|
||||||
|
statsRef,
|
||||||
|
allChartsRef,
|
||||||
|
setExporting,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
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,103 +208,108 @@ 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>
|
||||||
|
|
||||||
{/* Dashboard Stats */}
|
{/* Dashboard Stats */}
|
||||||
<DashboardStats data={dashboardProductionData?.statistics_data ?? []} />
|
<div ref={statsRef}>
|
||||||
|
<DashboardStats
|
||||||
|
data={dashboardProductionData?.statistics_data ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Use DashboardLineChart component or skeleton */}
|
{/* Use DashboardLineChart component or skeleton */}
|
||||||
{isLoadingDashboardProductionData ? (
|
<div ref={chartRef}>
|
||||||
<DashboardLineChartSkeleton />
|
{isLoadingDashboardProductionData ? (
|
||||||
) : dashboardProductionData &&
|
<DashboardLineChartSkeleton />
|
||||||
dashboardProductionData.charts &&
|
) : dashboardProductionData &&
|
||||||
Object.keys(dashboardProductionData.charts).length > 0 ? (
|
dashboardProductionData.charts &&
|
||||||
<DashboardLineChart
|
Object.keys(dashboardProductionData.charts).length > 0 ? (
|
||||||
analysisMode={
|
<DashboardLineChart
|
||||||
isResponseSuccess(dashboardProductionResponse)
|
analysisMode={
|
||||||
? dashboardProductionResponse.meta
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
? (
|
? dashboardProductionResponse.meta
|
||||||
dashboardProductionResponse.meta as unknown as DashboardMeta
|
? (
|
||||||
).filters?.analysis_mode
|
dashboardProductionResponse.meta as unknown as DashboardMeta
|
||||||
|
).filters?.analysis_mode
|
||||||
|
: analysisMode
|
||||||
: analysisMode
|
: analysisMode
|
||||||
: analysisMode
|
}
|
||||||
}
|
data={dashboardProductionData}
|
||||||
data={dashboardProductionData}
|
selectedKandang={
|
||||||
/>
|
analysisMode === 'OVERVIEW'
|
||||||
) : (
|
? (formik.values.kandang as OptionType)
|
||||||
<DashboardLineChartSkeleton
|
: undefined
|
||||||
meta={
|
}
|
||||||
isResponseSuccess(dashboardProductionResponse)
|
/>
|
||||||
? (dashboardProductionResponse.meta as unknown as DashboardMeta)
|
) : (
|
||||||
: undefined
|
<DashboardLineChartSkeleton
|
||||||
}
|
meta={
|
||||||
/>
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
|
? (dashboardProductionResponse.meta as unknown as DashboardMeta)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden container for all charts (used for PDF export in OVERVIEW mode) */}
|
||||||
|
{dashboardProductionData && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-9999px',
|
||||||
|
top: 0,
|
||||||
|
width: '1200px', // Fixed width for consistent PDF rendering
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DashboardAllCharts
|
||||||
|
ref={allChartsRef}
|
||||||
|
data={dashboardProductionData}
|
||||||
|
analysisMode={
|
||||||
|
isResponseSuccess(dashboardProductionResponse)
|
||||||
|
? dashboardProductionResponse.meta
|
||||||
|
? (
|
||||||
|
dashboardProductionResponse.meta as unknown as DashboardMeta
|
||||||
|
).filters?.analysis_mode
|
||||||
|
: analysisMode
|
||||||
|
: analysisMode
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -287,7 +344,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 +359,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 +440,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 +481,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 +511,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 +528,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'>
|
||||||
@@ -473,7 +538,6 @@ const DashboardProduction = () => {
|
|||||||
type='reset'
|
type='reset'
|
||||||
variant='soft'
|
variant='soft'
|
||||||
className='ms-4 min-w-36 rounded-lg'
|
className='ms-4 min-w-36 rounded-lg'
|
||||||
onClick={handleResetFilter}
|
|
||||||
>
|
>
|
||||||
Reset Filter
|
Reset Filter
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import Card from '@/components/Card';
|
||||||
|
import {
|
||||||
|
Dashboard,
|
||||||
|
DashboardOverviewCharts,
|
||||||
|
DashboardComparisonCharts,
|
||||||
|
DashboardChartsSeries,
|
||||||
|
DashboardChartsDataset,
|
||||||
|
} from '@/types/api/dashboard/dashboard';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
CartesianGrid,
|
||||||
|
Line,
|
||||||
|
LineChart,
|
||||||
|
ResponsiveContainer,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
} from 'recharts';
|
||||||
|
|
||||||
|
type DashboardAllChartsProps = {
|
||||||
|
data: Dashboard;
|
||||||
|
analysisMode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DashboardAllChartsRef = {
|
||||||
|
getChartRefs: () => {
|
||||||
|
key: string;
|
||||||
|
ref: HTMLDivElement | null;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type guard to check if charts is DashboardOverviewCharts
|
||||||
|
function isOverviewCharts(
|
||||||
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
|
): charts is DashboardOverviewCharts {
|
||||||
|
if (!charts) return false;
|
||||||
|
return (
|
||||||
|
'deplesi' in charts ||
|
||||||
|
'body_weight' in charts ||
|
||||||
|
'fcr' in charts ||
|
||||||
|
'performance' in charts ||
|
||||||
|
'quality_control' in charts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guard to check if charts is DashboardComparisonCharts
|
||||||
|
function isComparisonCharts(
|
||||||
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
|
): charts is DashboardComparisonCharts {
|
||||||
|
if (!charts) return false;
|
||||||
|
return 'farm' in charts || 'flock' in charts || 'kandang' in charts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineColors: Record<string, string> = {
|
||||||
|
body_weight: '#10B981',
|
||||||
|
std_body_weight: '#10B981',
|
||||||
|
act_laying: '#1062B9',
|
||||||
|
std_laying: '#1062B9',
|
||||||
|
act_egg_weight: '#10B981',
|
||||||
|
std_egg_weight: '#10B981',
|
||||||
|
act_feed_intake: '#F52419',
|
||||||
|
std_feed_intake: '#F52419',
|
||||||
|
act_uniformity: '#F59E0B',
|
||||||
|
std_uniformity: '#F59E0B',
|
||||||
|
act_fcr: '#10B981',
|
||||||
|
std_fcr: '#10B981',
|
||||||
|
act_fcr_cum: '#F52419',
|
||||||
|
std_fcr_cum: '#10B981',
|
||||||
|
normal: '#10B981',
|
||||||
|
abnormal: '#F52419',
|
||||||
|
act_deplesi: '#10B981',
|
||||||
|
std_deplesi: '#10B981',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultLineColors: string[] = [
|
||||||
|
'#10B981',
|
||||||
|
'#1062B9',
|
||||||
|
'#F52419',
|
||||||
|
'#F59E0B',
|
||||||
|
'#7F56D9',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper function to get line color
|
||||||
|
const getLineColor = (seriesId: string | number, index: number): string => {
|
||||||
|
const predefinedColor = lineColors[seriesId];
|
||||||
|
if (predefinedColor) {
|
||||||
|
return predefinedColor;
|
||||||
|
}
|
||||||
|
return defaultLineColors[index % defaultLineColors.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping for chart type labels
|
||||||
|
const chartTypeLabels: Record<keyof DashboardOverviewCharts, string> = {
|
||||||
|
body_weight: 'Body Weight',
|
||||||
|
performance: 'Performance',
|
||||||
|
fcr: 'FCR',
|
||||||
|
quality_control: 'Quality Control',
|
||||||
|
deplesi: 'Deplesi',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardAllCharts = forwardRef<
|
||||||
|
DashboardAllChartsRef,
|
||||||
|
DashboardAllChartsProps
|
||||||
|
>(({ data, analysisMode }, ref) => {
|
||||||
|
// Create refs for charts - use string keys for flexibility
|
||||||
|
const chartRefs = useRef<{
|
||||||
|
[key: string]: HTMLDivElement | null;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// Determine chart keys and labels based on analysis mode
|
||||||
|
const getChartConfig = () => {
|
||||||
|
if (analysisMode === 'OVERVIEW' && isOverviewCharts(data.charts)) {
|
||||||
|
const overviewKeys: (keyof DashboardOverviewCharts)[] = [
|
||||||
|
'body_weight',
|
||||||
|
'performance',
|
||||||
|
'fcr',
|
||||||
|
'quality_control',
|
||||||
|
'deplesi',
|
||||||
|
];
|
||||||
|
return overviewKeys.map((key) => ({
|
||||||
|
key,
|
||||||
|
label: chartTypeLabels[key],
|
||||||
|
chartData: (data.charts as DashboardOverviewCharts)[key],
|
||||||
|
}));
|
||||||
|
} else if (
|
||||||
|
analysisMode === 'COMPARISON' &&
|
||||||
|
isComparisonCharts(data.charts)
|
||||||
|
) {
|
||||||
|
// For comparison mode, find which comparison type has data
|
||||||
|
const comparisonKey = data.charts.farm
|
||||||
|
? 'farm'
|
||||||
|
: data.charts.flock
|
||||||
|
? 'flock'
|
||||||
|
: 'kandang';
|
||||||
|
|
||||||
|
const comparisonLabels: Record<string, string> = {
|
||||||
|
farm: 'Farm Comparison',
|
||||||
|
flock: 'Flock Comparison',
|
||||||
|
kandang: 'Kandang Comparison',
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: comparisonKey,
|
||||||
|
label: comparisonLabels[comparisonKey],
|
||||||
|
chartData: data.charts[comparisonKey],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartConfig = getChartConfig();
|
||||||
|
|
||||||
|
// Expose method to get all chart refs
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getChartRefs: () => {
|
||||||
|
return chartConfig
|
||||||
|
.map(({ key, label }) => ({
|
||||||
|
key,
|
||||||
|
ref: chartRefs.current[key] || null,
|
||||||
|
label,
|
||||||
|
}))
|
||||||
|
.filter((item) => item.ref !== null);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='space-y-6'>
|
||||||
|
{chartConfig.map(({ key, label, chartData }) => {
|
||||||
|
if (
|
||||||
|
!chartData ||
|
||||||
|
!chartData.dataset ||
|
||||||
|
chartData.dataset.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesData: DashboardChartsSeries[] = chartData.series || [];
|
||||||
|
const dataset: DashboardChartsDataset[] = chartData.dataset || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
ref={(el: HTMLDivElement | null) => {
|
||||||
|
chartRefs.current[key] = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={{
|
||||||
|
wrapper: 'w-full rounded-lg',
|
||||||
|
}}
|
||||||
|
variant='bordered'
|
||||||
|
>
|
||||||
|
<div className='flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6'>
|
||||||
|
<div className='text-lg font-semibold'>
|
||||||
|
{label}{' '}
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:information-circle'
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className='inline text-neutral-500'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className='flex flex-wrap gap-3 mb-6'>
|
||||||
|
{seriesData.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={series.id}
|
||||||
|
className='flex items-center gap-2 px-3 py-2 rounded-lg border border-neutral-400 bg-neutral-50'
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-6 h-0.5 ${
|
||||||
|
isStandard ? 'border-t-2 border-dashed' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: isStandard
|
||||||
|
? 'transparent'
|
||||||
|
: getLineColor(series.id, index),
|
||||||
|
borderColor: isStandard
|
||||||
|
? getLineColor(series.id, index)
|
||||||
|
: 'transparent',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<span className='text-sm text-neutral-900 font-medium'>
|
||||||
|
{series.label}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
icon='heroicons:information-circle'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='text-neutral-400'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chart */}
|
||||||
|
<ResponsiveContainer width='100%' height={350}>
|
||||||
|
<LineChart
|
||||||
|
data={dataset}
|
||||||
|
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={(() => {
|
||||||
|
const allValues: number[] = [];
|
||||||
|
dataset.forEach((item: DashboardChartsDataset) => {
|
||||||
|
seriesData.forEach((series) => {
|
||||||
|
const value = item[series.id];
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
allValues.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allValues.length === 0) return [0, 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);
|
||||||
|
|
||||||
|
return [domainMin, domainMax];
|
||||||
|
})()}
|
||||||
|
/>
|
||||||
|
{seriesData.map((series, index) => {
|
||||||
|
const isStandard = series.id
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes('std');
|
||||||
|
const dataKey = series.id.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={series.id}
|
||||||
|
type='monotone'
|
||||||
|
dataKey={dataKey}
|
||||||
|
name={series.label}
|
||||||
|
stroke={getLineColor(series.id, index)}
|
||||||
|
opacity={isStandard ? 0.5 : 1}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeDasharray={isStandard ? '5 5' : undefined}
|
||||||
|
dot={
|
||||||
|
isStandard
|
||||||
|
? false
|
||||||
|
: {
|
||||||
|
r: 3,
|
||||||
|
fill: '#fff',
|
||||||
|
stroke: getLineColor(series.id, index),
|
||||||
|
strokeWidth: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
activeDot={isStandard ? undefined : { r: 5 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DashboardAllCharts.displayName = 'DashboardAllCharts';
|
||||||
|
|
||||||
|
export default DashboardAllCharts;
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/Dropdown';
|
import Dropdown from '@/components/Dropdown';
|
||||||
|
import { OptionType } from '@/components/input/SelectInput';
|
||||||
import Menu from '@/components/menu/Menu';
|
import Menu from '@/components/menu/Menu';
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
import MenuItem from '@/components/menu/MenuItem';
|
||||||
|
import { formatNumber } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
Dashboard,
|
Dashboard,
|
||||||
DashboardOverviewCharts,
|
DashboardOverviewCharts,
|
||||||
@@ -25,20 +27,29 @@ import {
|
|||||||
type DashboardLineChartProps = {
|
type DashboardLineChartProps = {
|
||||||
analysisMode: 'OVERVIEW' | 'COMPARISON';
|
analysisMode: 'OVERVIEW' | 'COMPARISON';
|
||||||
data: Dashboard;
|
data: Dashboard;
|
||||||
|
selectedKandang?: OptionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type guard to check if charts is DashboardOverviewCharts
|
// Type guard to check if charts is DashboardOverviewCharts
|
||||||
function isOverviewCharts(
|
function isOverviewCharts(
|
||||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
): charts is DashboardOverviewCharts {
|
): charts is DashboardOverviewCharts {
|
||||||
return 'deplesi' in charts;
|
if (!charts) return false;
|
||||||
|
return (
|
||||||
|
'deplesi' in charts ||
|
||||||
|
'body_weight' in charts ||
|
||||||
|
'fcr' in charts ||
|
||||||
|
'performance' in charts ||
|
||||||
|
'quality_control' in charts
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type guard to check if charts is DashboardComparisonCharts
|
// Type guard to check if charts is DashboardComparisonCharts
|
||||||
function isComparisonCharts(
|
function isComparisonCharts(
|
||||||
charts: DashboardOverviewCharts | DashboardComparisonCharts
|
charts: DashboardOverviewCharts | DashboardComparisonCharts | undefined
|
||||||
): charts is DashboardComparisonCharts {
|
): charts is DashboardComparisonCharts {
|
||||||
return 'location' in charts || 'flock' in charts || 'kandang' in charts;
|
if (!charts) return false;
|
||||||
|
return 'farm' in charts || 'flock' in charts || 'kandang' in charts;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineColors: Record<string, string> = {
|
const lineColors: Record<string, string> = {
|
||||||
@@ -94,6 +105,7 @@ const getLineColor = (
|
|||||||
const DashboardLineChart = ({
|
const DashboardLineChart = ({
|
||||||
analysisMode,
|
analysisMode,
|
||||||
data,
|
data,
|
||||||
|
selectedKandang,
|
||||||
}: DashboardLineChartProps) => {
|
}: DashboardLineChartProps) => {
|
||||||
const [chartData, setChartData] =
|
const [chartData, setChartData] =
|
||||||
useState<keyof DashboardOverviewCharts>('body_weight');
|
useState<keyof DashboardOverviewCharts>('body_weight');
|
||||||
@@ -123,7 +135,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +236,7 @@ const DashboardLineChart = ({
|
|||||||
isComparisonCharts(data.charts)
|
isComparisonCharts(data.charts)
|
||||||
) {
|
) {
|
||||||
const comparisonChart =
|
const comparisonChart =
|
||||||
data.charts.location || data.charts.flock || data.charts.kandang;
|
data.charts.farm || data.charts.flock || data.charts.kandang;
|
||||||
seriesData = comparisonChart?.series || [];
|
seriesData = comparisonChart?.series || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,261 +295,382 @@ 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.farm ||
|
||||||
|
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.farm ||
|
||||||
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.farm ||
|
||||||
|
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}`}
|
||||||
|
content={(props) => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col gap-2 rounded-lg bg-neutral-950 p-4 text-white'>
|
||||||
|
<p className='text-neutral-300 text-xs font-semibold text-start'>
|
||||||
|
{analysisMode === 'OVERVIEW'
|
||||||
|
? selectedKandang
|
||||||
|
? selectedKandang.label || 'Overview Performance'
|
||||||
|
: 'Overview Performance'
|
||||||
|
: 'Comparison Performance'}
|
||||||
|
</p>
|
||||||
|
<ul className='flex flex-col gap-1'>
|
||||||
|
{props.payload.map((item, index) => {
|
||||||
|
if (item.name.startsWith('STD. ')) return null;
|
||||||
|
// 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.farm ||
|
||||||
|
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 === item.name
|
||||||
|
);
|
||||||
|
const color = series?.id
|
||||||
|
? getLineColor(series.id, index, analysisMode)
|
||||||
|
: '#9ca3af';
|
||||||
|
const unit = series?.unit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.name}
|
||||||
|
className='flex w-full justify-between items-center flex-row gap-6 p-0'
|
||||||
|
>
|
||||||
|
<span className='flex flex-row gap-1 items-center'>
|
||||||
|
<div
|
||||||
|
className='h-4 w-4 m-0 rounded-md'
|
||||||
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div className='m-0'>
|
||||||
|
{formatNumber(item.value)}
|
||||||
|
{unit}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span className='m-0'>{item.name}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p className='text-neutral-300 text-xs text-start'>
|
||||||
|
Week {props.label}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}}
|
||||||
})()}
|
formatter={(
|
||||||
</LineChart>
|
value: number | undefined,
|
||||||
</ResponsiveContainer>
|
name: string | undefined
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
name === undefined ||
|
||||||
|
name.startsWith('STD. ')
|
||||||
|
)
|
||||||
|
return [undefined, undefined];
|
||||||
|
|
||||||
|
// 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.farm ||
|
||||||
|
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 id = series?.id || '';
|
||||||
|
|
||||||
|
return [value, id];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* 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.farm || 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.farm || 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 */}
|
||||||
|
<h3 className='text-gray-900 font-semibold text-base mb-2'>
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import jsPDF from 'jspdf';
|
||||||
|
import { toPng } from 'html-to-image';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { formatDate } from '@/lib/helper';
|
||||||
|
import { DashboardFilterType } from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||||
|
import { DashboardAllChartsRef } from '@/components/pages/dashboard/chart/DashboardAllCharts';
|
||||||
|
|
||||||
|
interface DashboardPDFExportParams {
|
||||||
|
filterValues: DashboardFilterType;
|
||||||
|
statsRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
allChartsRef: React.RefObject<DashboardAllChartsRef | null>;
|
||||||
|
setExporting: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateDashboardPDF = async ({
|
||||||
|
filterValues,
|
||||||
|
statsRef,
|
||||||
|
allChartsRef,
|
||||||
|
setExporting,
|
||||||
|
}: DashboardPDFExportParams): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setExporting(true);
|
||||||
|
toast.loading('Generating PDF...', { id: 'export-pdf' });
|
||||||
|
|
||||||
|
// Wait for DOM to update
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
||||||
|
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
const margin = 10;
|
||||||
|
let yPosition = margin;
|
||||||
|
|
||||||
|
// Add title
|
||||||
|
pdf.setFontSize(16);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
pdf.text('Dashboard Produksi', margin, yPosition);
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Add filter information (horizontal layout)
|
||||||
|
pdf.setFontSize(6);
|
||||||
|
pdf.setFont('helvetica', 'normal');
|
||||||
|
|
||||||
|
const filterItems: string[] = [];
|
||||||
|
|
||||||
|
// Period
|
||||||
|
if (filterValues.startDate || filterValues.endDate) {
|
||||||
|
const periodText = `Periode: ${
|
||||||
|
filterValues.startDate
|
||||||
|
? formatDate(filterValues.startDate, 'DD MMM YYYY')
|
||||||
|
: '-'
|
||||||
|
} s.d ${
|
||||||
|
filterValues.endDate
|
||||||
|
? formatDate(filterValues.endDate, 'DD MMM YYYY')
|
||||||
|
: '-'
|
||||||
|
}`;
|
||||||
|
filterItems.push(periodText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analysis Mode
|
||||||
|
const analysisModeText = `Analysis Mode: ${
|
||||||
|
filterValues.analysisMode === 'OVERVIEW'
|
||||||
|
? 'Performance Overview'
|
||||||
|
: 'Performance Comparison'
|
||||||
|
}`;
|
||||||
|
filterItems.push(analysisModeText);
|
||||||
|
|
||||||
|
// Comparison Type (only for COMPARISON mode)
|
||||||
|
if (
|
||||||
|
filterValues.analysisMode === 'COMPARISON' &&
|
||||||
|
filterValues.comparisonType
|
||||||
|
) {
|
||||||
|
const comparisonTypeLabel =
|
||||||
|
filterValues.comparisonType === 'FARM'
|
||||||
|
? 'Farm'
|
||||||
|
: filterValues.comparisonType === 'FLOCK'
|
||||||
|
? 'Flock'
|
||||||
|
: filterValues.comparisonType === 'KANDANG'
|
||||||
|
? 'Kandang'
|
||||||
|
: filterValues.comparisonType;
|
||||||
|
filterItems.push(`Compared By: ${comparisonTypeLabel}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Farm
|
||||||
|
if (filterValues.location) {
|
||||||
|
const locationText = Array.isArray(filterValues.location)
|
||||||
|
? filterValues.location.map((loc) => loc.label).join(', ')
|
||||||
|
: filterValues.location.label;
|
||||||
|
filterItems.push(`Farm: ${locationText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flock
|
||||||
|
if (
|
||||||
|
filterValues.flock &&
|
||||||
|
(Array.isArray(filterValues.flock)
|
||||||
|
? filterValues.flock.length > 0
|
||||||
|
: filterValues.flock)
|
||||||
|
) {
|
||||||
|
const flockText = Array.isArray(filterValues.flock)
|
||||||
|
? filterValues.flock.map((f) => f.label).join(', ')
|
||||||
|
: filterValues.flock.label;
|
||||||
|
filterItems.push(`Flock: ${flockText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kandang
|
||||||
|
if (
|
||||||
|
filterValues.kandang &&
|
||||||
|
(Array.isArray(filterValues.kandang)
|
||||||
|
? filterValues.kandang.length > 0
|
||||||
|
: filterValues.kandang)
|
||||||
|
) {
|
||||||
|
const kandangText = Array.isArray(filterValues.kandang)
|
||||||
|
? filterValues.kandang.map((k) => k.label).join(', ')
|
||||||
|
: filterValues.kandang.label;
|
||||||
|
filterItems.push(`Kandang: ${kandangText || '-'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generated timestamp
|
||||||
|
filterItems.push(`Dicetak: ${formatDate(new Date(), 'DD MMM YYYY HH:mm')}`);
|
||||||
|
|
||||||
|
// Render filter items horizontally with word wrap and gray background
|
||||||
|
const maxWidth = pageWidth - 2 * margin;
|
||||||
|
let currentLine = '';
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// First pass: calculate all lines
|
||||||
|
filterItems.forEach((item, index) => {
|
||||||
|
const separator = index > 0 ? ' | ' : '';
|
||||||
|
const testLine = currentLine + separator + item;
|
||||||
|
const testWidth = pdf.getTextWidth(testLine);
|
||||||
|
|
||||||
|
if (testWidth > maxWidth && currentLine !== '') {
|
||||||
|
lines.push(currentLine);
|
||||||
|
currentLine = item;
|
||||||
|
} else {
|
||||||
|
currentLine = testLine;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add last line
|
||||||
|
if (currentLine) {
|
||||||
|
lines.push(currentLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate background dimensions
|
||||||
|
const lineHeight = 3;
|
||||||
|
const padding = 1;
|
||||||
|
const backgroundHeight = lines.length * lineHeight + padding * 2;
|
||||||
|
|
||||||
|
// Draw gray background
|
||||||
|
pdf.setFillColor(240, 240, 240); // Light gray (RGB: 240, 240, 240)
|
||||||
|
pdf.rect(
|
||||||
|
margin - padding,
|
||||||
|
yPosition - padding - 2,
|
||||||
|
pageWidth - 2 * margin + padding * 2,
|
||||||
|
backgroundHeight,
|
||||||
|
'F'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render text on top of background
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
pdf.text(line, margin, yPosition);
|
||||||
|
if (index < lines.length - 1) {
|
||||||
|
yPosition += lineHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yPosition += 10;
|
||||||
|
|
||||||
|
// Capture and add stats if available
|
||||||
|
if (statsRef.current) {
|
||||||
|
const statsImage = await toPng(statsRef.current, {
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: 2,
|
||||||
|
});
|
||||||
|
const statsImgProps = pdf.getImageProperties(statsImage);
|
||||||
|
const statsWidth = pageWidth - 2 * margin;
|
||||||
|
const statsHeight =
|
||||||
|
(statsImgProps.height * statsWidth) / statsImgProps.width;
|
||||||
|
|
||||||
|
// Check if we need a new page
|
||||||
|
if (yPosition + statsHeight > pageHeight - margin) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.addImage(
|
||||||
|
statsImage,
|
||||||
|
'PNG',
|
||||||
|
margin,
|
||||||
|
yPosition,
|
||||||
|
statsWidth,
|
||||||
|
statsHeight
|
||||||
|
);
|
||||||
|
yPosition += statsHeight + 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allChartsRef.current) {
|
||||||
|
// Get all individual chart refs
|
||||||
|
const chartRefs = allChartsRef.current.getChartRefs();
|
||||||
|
|
||||||
|
// Capture each chart separately and add to PDF
|
||||||
|
for (let i = 0; i < chartRefs.length; i++) {
|
||||||
|
const { ref: chartElement, label } = chartRefs[i];
|
||||||
|
|
||||||
|
if (chartElement) {
|
||||||
|
// Add chart title
|
||||||
|
pdf.setFontSize(12);
|
||||||
|
pdf.setFont('helvetica', 'bold');
|
||||||
|
|
||||||
|
const chartImage = await toPng(chartElement, {
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: 2,
|
||||||
|
});
|
||||||
|
const chartImgProps = pdf.getImageProperties(chartImage);
|
||||||
|
const chartWidth = pageWidth - 2 * margin;
|
||||||
|
const chartHeight =
|
||||||
|
(chartImgProps.height * chartWidth) / chartImgProps.width;
|
||||||
|
|
||||||
|
// Calculate total height needed (title + spacing + chart)
|
||||||
|
const titleHeight = 10;
|
||||||
|
const totalHeight = titleHeight + chartHeight;
|
||||||
|
|
||||||
|
// Check if chart fits on current page
|
||||||
|
if (yPosition + totalHeight > pageHeight - margin) {
|
||||||
|
pdf.addPage();
|
||||||
|
yPosition = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add title
|
||||||
|
pdf.text(label, margin, yPosition);
|
||||||
|
yPosition += titleHeight;
|
||||||
|
|
||||||
|
// Add chart image
|
||||||
|
pdf.addImage(
|
||||||
|
chartImage,
|
||||||
|
'PNG',
|
||||||
|
margin,
|
||||||
|
yPosition,
|
||||||
|
chartWidth,
|
||||||
|
chartHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update yPosition for next chart (add spacing between charts)
|
||||||
|
yPosition += chartHeight + 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the PDF
|
||||||
|
const fileName = `dashboard-production-${new Date().toISOString().split('T')[0]}.pdf`;
|
||||||
|
pdf.save(fileName);
|
||||||
|
|
||||||
|
toast.success('PDF exported successfully!', { id: 'export-pdf' });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to export PDF. Please try again.', {
|
||||||
|
id: 'export-pdf',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ export type DashboardFilterType = {
|
|||||||
analysisMode: string;
|
analysisMode: string;
|
||||||
comparisonType: string | undefined;
|
comparisonType: string | undefined;
|
||||||
location: OptionType | OptionType[];
|
location: OptionType | OptionType[];
|
||||||
lokasiIds: number[] | undefined;
|
locationIds: number[] | undefined;
|
||||||
flock: OptionType | OptionType[] | undefined;
|
flock: OptionType | OptionType[] | undefined;
|
||||||
flockIds: number[] | undefined;
|
flockIds: number[] | undefined;
|
||||||
kandang: OptionType | OptionType[] | undefined;
|
kandang: OptionType | OptionType[] | undefined;
|
||||||
@@ -25,7 +25,7 @@ export const DashboardFilterOverviewSchema: yup.ObjectSchema<DashboardFilterType
|
|||||||
then: (schema) => schema.required('Compared by is required'),
|
then: (schema) => schema.required('Compared by is required'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
}),
|
}),
|
||||||
lokasiIds: yup.array().optional(),
|
locationIds: yup.array().optional(),
|
||||||
flockIds: yup.array().optional(),
|
flockIds: yup.array().optional(),
|
||||||
kandangIds: yup.array().optional(),
|
kandangIds: yup.array().optional(),
|
||||||
location: yup
|
location: yup
|
||||||
@@ -68,7 +68,7 @@ export const DashboardFilterComparisonSchema: yup.ObjectSchema<DashboardFilterTy
|
|||||||
then: (schema) => schema.required('Compared by is required'),
|
then: (schema) => schema.required('Compared by is required'),
|
||||||
otherwise: (schema) => schema.optional(),
|
otherwise: (schema) => schema.optional(),
|
||||||
}),
|
}),
|
||||||
lokasiIds: yup.array().optional(),
|
locationIds: yup.array().optional(),
|
||||||
flockIds: yup.array().optional(),
|
flockIds: yup.array().optional(),
|
||||||
kandangIds: yup.array().optional(),
|
kandangIds: yup.array().optional(),
|
||||||
location: yup
|
location: yup
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-7xl pb-16'>
|
<section className='w-full max-w-full pb-16'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -65,7 +65,7 @@ const ExpenseDetail: React.FC<ExpenseDetailProps> = ({ initialValues }) => {
|
|||||||
tabs={expenseDetailTabs}
|
tabs={expenseDetailTabs}
|
||||||
variant='lifted'
|
variant='lifted'
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'max-w-5xl mx-auto mt-4',
|
wrapper: 'mx-auto mt-4',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ const ExpenseRealizationContent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
|
||||||
<RequirePermission permissions='lti.expense.update.realization'>
|
<RequirePermission permissions='lti.expense.update.realization'>
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +84,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
<div className='overflow-x-auto w-full mx-auto'>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -179,7 +179,7 @@ const ExpenseRealizationContent = ({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto'>
|
||||||
<div className='flex flex-row gap-4'>
|
<div className='flex flex-row gap-4'>
|
||||||
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
<Card variant='bordered' size='sm' className={{ wrapper: 'grow' }}>
|
||||||
<div className='w-full flex flex-col gap-2'>
|
<div className='w-full flex flex-col gap-2'>
|
||||||
@@ -216,127 +216,141 @@ const ExpenseRealizationContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto grid grid-cols-2 gap-4'>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<div>
|
||||||
Rincian Pengajuan Biaya Operasional
|
<h2 className='font-bold text-xl text-center'>
|
||||||
</h2>
|
Rincian Pengajuan Biaya Operasional
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
let expenseGrandTotal = 0;
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.pengajuans?.forEach(
|
kandangExpense.pengajuans?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={kandangExpenseIdx}
|
key={kandangExpenseIdx}
|
||||||
className='overflow-x-auto w-full mx-auto'
|
className='overflow-x-auto w-full mx-auto'
|
||||||
>
|
>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
className='font-bold text-center text-base-content text-lg'
|
className='font-bold text-center text-base-content text-lg'
|
||||||
>
|
>
|
||||||
Biaya {kandangExpense.name}
|
Biaya {kandangExpense.name}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nonstock</th>
|
|
||||||
<th>Total Kuantitas</th>
|
|
||||||
<th>Total Biaya</th>
|
|
||||||
<th>Catatan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{kandangExpense.pengajuans?.map(
|
|
||||||
(pengajuanItem, pengajuanIdx) => (
|
|
||||||
<tr key={pengajuanIdx}>
|
|
||||||
<td>{pengajuanItem.nonstock.name}</td>
|
|
||||||
<td>{pengajuanItem.qty}</td>
|
|
||||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
|
||||||
<td className='w-xs'>{pengajuanItem.note ?? '-'}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)
|
<tr>
|
||||||
)}
|
<th>Nonstock</th>
|
||||||
</tbody>
|
<th>Total Kuantitas</th>
|
||||||
<tfoot>
|
<th>Total Biaya</th>
|
||||||
<tr className='border-y'>
|
<th>Catatan</th>
|
||||||
<th colSpan={2} className='text-right'>
|
</tr>
|
||||||
Total Biaya Keseluruhan:
|
</thead>
|
||||||
</th>
|
<tbody>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
{kandangExpense.pengajuans?.map(
|
||||||
</tr>
|
(pengajuanItem, pengajuanIdx) => (
|
||||||
</tfoot>
|
<tr key={pengajuanIdx}>
|
||||||
</table>
|
<td>{pengajuanItem.nonstock.name}</td>
|
||||||
</div>
|
<td>{pengajuanItem.qty}</td>
|
||||||
);
|
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||||
})}
|
<td className='w-xs'>
|
||||||
|
{pengajuanItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className='border-y'>
|
||||||
|
<th colSpan={2} className='text-right'>
|
||||||
|
Total Biaya Keseluruhan:
|
||||||
|
</th>
|
||||||
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Realisasi Biaya Operasional
|
Rincian Realisasi Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className='w-full mt-2 flex flex-col gap-4'>
|
<div className='w-full mt-2 flex flex-col gap-4'>
|
||||||
{initialValues?.kandangs.map((kandangExpense, kandangExpenseIdx) => {
|
{initialValues?.kandangs.map(
|
||||||
let expenseGrandTotal = 0;
|
(kandangExpense, kandangExpenseIdx) => {
|
||||||
|
let expenseGrandTotal = 0;
|
||||||
|
|
||||||
kandangExpense.realisasi?.forEach(
|
kandangExpense.realisasi?.forEach(
|
||||||
(item) => (expenseGrandTotal += item.qty * item.price)
|
(item) => (expenseGrandTotal += item.qty * item.price)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={kandangExpenseIdx}
|
key={kandangExpenseIdx}
|
||||||
className='overflow-x-auto w-full mx-auto'
|
className='overflow-x-auto w-full mx-auto'
|
||||||
>
|
>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
colSpan={5}
|
colSpan={5}
|
||||||
className='font-bold text-center text-base-content text-lg'
|
className='font-bold text-center text-base-content text-lg'
|
||||||
>
|
>
|
||||||
Biaya {kandangExpense.name}
|
Biaya {kandangExpense.name}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Nonstock</th>
|
|
||||||
<th>Total Kuantitas</th>
|
|
||||||
<th>Total Biaya</th>
|
|
||||||
<th>Catatan</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{kandangExpense.realisasi?.map(
|
|
||||||
(realisasiItem, realisasiIdx) => (
|
|
||||||
<tr key={realisasiIdx}>
|
|
||||||
<td>{realisasiItem.nonstock.name}</td>
|
|
||||||
<td>{realisasiItem.qty}</td>
|
|
||||||
<td>{formatCurrency(realisasiItem.price)}</td>
|
|
||||||
<td className='w-xs'>{realisasiItem.note ?? '-'}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)
|
<tr>
|
||||||
)}
|
<th>Nonstock</th>
|
||||||
</tbody>
|
<th>Total Kuantitas</th>
|
||||||
<tfoot>
|
<th>Total Biaya</th>
|
||||||
<tr className='border-y'>
|
<th>Catatan</th>
|
||||||
<th colSpan={2} className='text-right'>
|
</tr>
|
||||||
Total Biaya Keseluruhan:
|
</thead>
|
||||||
</th>
|
<tbody>
|
||||||
<th colSpan={2}>{formatCurrency(expenseGrandTotal)}</th>
|
{kandangExpense.realisasi?.map(
|
||||||
</tr>
|
(realisasiItem, realisasiIdx) => (
|
||||||
</tfoot>
|
<tr key={realisasiIdx}>
|
||||||
</table>
|
<td>{realisasiItem.nonstock.name}</td>
|
||||||
</div>
|
<td>{realisasiItem.qty}</td>
|
||||||
);
|
<td>{formatCurrency(realisasiItem.price)}</td>
|
||||||
})}
|
<td className='w-xs'>
|
||||||
|
{realisasiItem.notes ?? '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className='border-y'>
|
||||||
|
<th colSpan={2} className='text-right'>
|
||||||
|
Total Biaya Keseluruhan:
|
||||||
|
</th>
|
||||||
|
<th colSpan={2}>
|
||||||
|
{formatCurrency(expenseGrandTotal)}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ const ExpenseRequestContent = ({
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{initialValues && !isLoadingApprovalHistory && approvalHistory && (
|
{initialValues && !isLoadingApprovalHistory && approvalHistory && (
|
||||||
<div className='w-full max-w-5xl my-4 mx-auto'>
|
<div className='w-full my-4 mx-auto'>
|
||||||
<ApprovalSteps approvals={approvalHistory} />
|
<ApprovalSteps approvals={approvalHistory} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -281,7 +281,7 @@ const ExpenseRequestContent = ({
|
|||||||
<div className='w-full mt-4 flex flex-col gap-4'>
|
<div className='w-full mt-4 flex flex-col gap-4'>
|
||||||
{/* TODO: apply RBAC */}
|
{/* TODO: apply RBAC */}
|
||||||
|
|
||||||
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
<div className='w-full mx-auto flex flex-col sm:flex-row justify-end gap-2'>
|
||||||
{isCurrentApprovalOnHeadArea && (
|
{isCurrentApprovalOnHeadArea && (
|
||||||
<RequirePermission permissions='lti.expense.approve.head_area'>
|
<RequirePermission permissions='lti.expense.approve.head_area'>
|
||||||
<Button
|
<Button
|
||||||
@@ -414,7 +414,7 @@ const ExpenseRequestContent = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='overflow-x-auto w-full max-w-5xl mx-auto'>
|
<div className='overflow-x-auto w-full mx-auto'>
|
||||||
<table className='table table-sm table-zebra'>
|
<table className='table table-sm table-zebra'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -608,7 +608,7 @@ const ExpenseRequestContent = ({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full max-w-5xl mt-8 mx-auto'>
|
<div className='w-full mt-8 mx-auto'>
|
||||||
<h2 className='font-bold text-xl text-center'>
|
<h2 className='font-bold text-xl text-center'>
|
||||||
Rincian Pengajuan Biaya Operasional
|
Rincian Pengajuan Biaya Operasional
|
||||||
</h2>
|
</h2>
|
||||||
@@ -654,7 +654,7 @@ const ExpenseRequestContent = ({
|
|||||||
<td>{pengajuanItem.qty}</td>
|
<td>{pengajuanItem.qty}</td>
|
||||||
<td>{formatCurrency(pengajuanItem.price)}</td>
|
<td>{formatCurrency(pengajuanItem.price)}</td>
|
||||||
<td className='w-xs'>
|
<td className='w-xs'>
|
||||||
{pengajuanItem.note ?? '-'}
|
{pengajuanItem.notes ?? '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,17 +54,19 @@ const RowOptionsMenu = ({
|
|||||||
rejectClickHandler: () => void;
|
rejectClickHandler: () => void;
|
||||||
deleteClickHandler: () => void;
|
deleteClickHandler: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const showEditButton =
|
const showEditButton = props.row.original.latest_approval
|
||||||
props.row.original.latest_approval.step_number !== 6 &&
|
? props.row.original.latest_approval.step_number !== 6 &&
|
||||||
(props.row.original.latest_approval.step_number === 1 ||
|
(props.row.original.latest_approval.step_number === 1 ||
|
||||||
props.row.original.latest_approval.step_number === 2 ||
|
props.row.original.latest_approval.step_number === 2 ||
|
||||||
props.row.original.latest_approval.step_number === 3 ||
|
props.row.original.latest_approval.step_number === 3 ||
|
||||||
props.row.original.latest_approval.step_number === 4);
|
props.row.original.latest_approval.step_number === 4)
|
||||||
|
: false;
|
||||||
|
|
||||||
// TODO: apply RBAC
|
// TODO: apply RBAC
|
||||||
const showRealizationButton =
|
const showRealizationButton = props.row.original.latest_approval
|
||||||
props.row.original.latest_approval.action !== 'REJECTED' &&
|
? props.row.original.latest_approval.action !== 'REJECTED' &&
|
||||||
props.row.original.latest_approval.step_number === 4;
|
props.row.original.latest_approval.step_number === 4
|
||||||
|
: false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
@@ -278,6 +280,7 @@ const ExpensesTable = () => {
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const isCheckboxDisabled =
|
const isCheckboxDisabled =
|
||||||
!row.getCanSelect() ||
|
!row.getCanSelect() ||
|
||||||
|
!row.original.latest_approval ||
|
||||||
row.original.latest_approval.action === 'REJECTED';
|
row.original.latest_approval.action === 'REJECTED';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -413,6 +416,8 @@ const ExpensesTable = () => {
|
|||||||
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
const tableEnableRowSelectionHandler: (row: Row<Expense>) => boolean = (
|
||||||
row
|
row
|
||||||
) => {
|
) => {
|
||||||
|
if (!row.original.latest_approval) return false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
row.original.latest_approval.action !== 'REJECTED' &&
|
row.original.latest_approval.action !== 'REJECTED' &&
|
||||||
row.original.latest_approval.step_number !== 6
|
row.original.latest_approval.step_number !== 6
|
||||||
@@ -692,14 +697,6 @@ const ExpensesTable = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DebouncedTextInput
|
|
||||||
name='search'
|
|
||||||
placeholder='Cari Biaya Operasional'
|
|
||||||
value={tableFilterState.search}
|
|
||||||
onChange={searchChangeHandler}
|
|
||||||
className={{ wrapper: 'sm:max-w-3xs' }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='grid grid-cols-12 justify-end gap-2'>
|
<div className='grid grid-cols-12 justify-end gap-2'>
|
||||||
@@ -753,17 +750,12 @@ const ExpensesTable = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectInput
|
<DebouncedTextInput
|
||||||
label='Baris'
|
name='search'
|
||||||
options={ROWS_OPTIONS}
|
placeholder='Cari Biaya Operasional'
|
||||||
value={{
|
value={tableFilterState.search}
|
||||||
label: String(tableFilterState.pageSize),
|
onChange={searchChangeHandler}
|
||||||
value: tableFilterState.pageSize,
|
className={{ wrapper: 'col-span-12 max-w-52 justify-self-end' }}
|
||||||
}}
|
|
||||||
onChange={pageSizeChangeHandler}
|
|
||||||
className={{
|
|
||||||
wrapper: 'col-span-12 max-w-28 justify-self-end',
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { isResponseSuccess } from '@/lib/api-helper';
|
|||||||
interface ExpenseKandangsTableProps {
|
interface ExpenseKandangsTableProps {
|
||||||
locationId?: number;
|
locationId?: number;
|
||||||
type: 'add' | 'edit' | 'detail';
|
type: 'add' | 'edit' | 'detail';
|
||||||
|
formType?: 'request' | 'realization';
|
||||||
selectedKandangs: {
|
selectedKandangs: {
|
||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -31,6 +32,7 @@ interface ExpenseKandangsTableProps {
|
|||||||
|
|
||||||
const ExpenseKandangsTable = ({
|
const ExpenseKandangsTable = ({
|
||||||
type,
|
type,
|
||||||
|
formType = 'request',
|
||||||
locationId,
|
locationId,
|
||||||
selectedKandangs,
|
selectedKandangs,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -172,69 +174,84 @@ const ExpenseKandangsTable = ({
|
|||||||
updateSortingFilter('picSort', picSortFilter);
|
updateSortingFilter('picSort', picSortFilter);
|
||||||
}, [sorting, updateSortingFilter]);
|
}, [sorting, updateSortingFilter]);
|
||||||
|
|
||||||
return (
|
// Tampilkan tabel jika:
|
||||||
<Card
|
// 1. Mode request pertama kali (type='add' dan formType='request')
|
||||||
className={{
|
// 2. Atau sudah ada kandang yang dipilih
|
||||||
wrapper: className?.wrapper,
|
const shouldShowTable =
|
||||||
body: 'p-4 shadow',
|
(type === 'add' && formType === 'request') ||
|
||||||
}}
|
(selectedKandangs.length > 0 && selectedKandangs.some((k) => k.id));
|
||||||
>
|
|
||||||
<Collapse
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
title={
|
|
||||||
<div className='card-actions p-4 justify-between items-center w-full'>
|
|
||||||
<div className='card-title'>Pilih Kandang</div>
|
|
||||||
|
|
||||||
<Icon
|
return (
|
||||||
icon='material-symbols:keyboard-arrow-down'
|
<>
|
||||||
width={24}
|
{shouldShowTable && (
|
||||||
height={24}
|
<Card
|
||||||
className={cn('text-primary transition-transform', {
|
|
||||||
'-rotate-180': open,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
className='w-full!'
|
|
||||||
titleClassName='w-full p-0!'
|
|
||||||
>
|
|
||||||
<Table<Kandang>
|
|
||||||
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
|
|
||||||
columns={kandangsColumns}
|
|
||||||
pageSize={tableFilterState.pageSize}
|
|
||||||
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
|
|
||||||
totalItems={
|
|
||||||
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
|
|
||||||
}
|
|
||||||
onPageChange={setPage}
|
|
||||||
isLoading={isLoading}
|
|
||||||
sorting={sorting}
|
|
||||||
setSorting={setSorting}
|
|
||||||
rowSelection={rowSelection}
|
|
||||||
setRowSelection={setRowSelection}
|
|
||||||
className={{
|
className={{
|
||||||
containerClassName: cn({
|
wrapper: className?.wrapper,
|
||||||
'mb-20':
|
body: 'p-4 shadow',
|
||||||
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
|
|
||||||
}),
|
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
|
||||||
headerColumnClassName:
|
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
|
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
|
||||||
bodyColumnClassName:
|
|
||||||
'px-6 py-3 first:flex first:flex-row first:justify-start',
|
|
||||||
paginationClassName: cn({
|
|
||||||
hidden:
|
|
||||||
isResponseSuccess(kandangs) &&
|
|
||||||
kandangs?.meta?.total_pages === 1,
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Collapse>
|
<Collapse
|
||||||
</Card>
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
title={
|
||||||
|
<div className='card-actions p-4 justify-between items-center w-full'>
|
||||||
|
<div className='card-title'>
|
||||||
|
{formType === 'realization'
|
||||||
|
? 'Kandang yang Direalisasikan'
|
||||||
|
: 'Pilih Kandang'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:keyboard-arrow-down'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className={cn('text-primary transition-transform', {
|
||||||
|
'-rotate-180': open,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
className='w-full!'
|
||||||
|
titleClassName='w-full p-0!'
|
||||||
|
>
|
||||||
|
<Table<Kandang>
|
||||||
|
data={isResponseSuccess(kandangs) ? kandangs?.data : []}
|
||||||
|
columns={kandangsColumns}
|
||||||
|
pageSize={tableFilterState.pageSize}
|
||||||
|
page={isResponseSuccess(kandangs) ? kandangs?.meta?.page : 0}
|
||||||
|
totalItems={
|
||||||
|
isResponseSuccess(kandangs) ? kandangs?.meta?.total_results : 0
|
||||||
|
}
|
||||||
|
onPageChange={setPage}
|
||||||
|
isLoading={isLoading}
|
||||||
|
sorting={sorting}
|
||||||
|
setSorting={setSorting}
|
||||||
|
rowSelection={rowSelection}
|
||||||
|
setRowSelection={setRowSelection}
|
||||||
|
className={{
|
||||||
|
containerClassName: cn({
|
||||||
|
'mb-20':
|
||||||
|
isResponseSuccess(kandangs) && kandangs?.data?.length === 0,
|
||||||
|
}),
|
||||||
|
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
||||||
|
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
||||||
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
|
headerColumnClassName:
|
||||||
|
'px-6 py-3 text-xs font-semibold text-gray-500 first:flex first:flex-row first:justify-start',
|
||||||
|
bodyRowClassName: 'border-b border-b-gray-200',
|
||||||
|
bodyColumnClassName:
|
||||||
|
'px-6 py-3 first:flex first:flex-row first:justify-start',
|
||||||
|
paginationClassName: cn({
|
||||||
|
hidden:
|
||||||
|
isResponseSuccess(kandangs) &&
|
||||||
|
kandangs?.meta?.total_pages === 1,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Collapse>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
? formatDate(initialValues?.realization_date, 'YYYY-MM-DD')
|
||||||
: undefined,
|
: undefined,
|
||||||
kandangs: initialValues?.kandangs.map((kandang) => ({
|
kandangs: initialValues?.kandangs.map((kandang) => ({
|
||||||
id: kandang.kandang_id,
|
id: kandang.id,
|
||||||
name: kandang.name,
|
name: kandang.name,
|
||||||
})),
|
})),
|
||||||
supplier: initialValues?.supplier
|
supplier: initialValues?.supplier
|
||||||
@@ -159,7 +159,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
},
|
},
|
||||||
quantity: realisasiItem.qty,
|
quantity: realisasiItem.qty,
|
||||||
price: realisasiItem.price,
|
price: realisasiItem.price,
|
||||||
notes: realisasiItem.note,
|
notes: realisasiItem.notes,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: kandangExpense.pengajuans
|
: kandangExpense.pengajuans
|
||||||
@@ -170,7 +170,7 @@ export const getExpenseRealizationFormInitialValues = (
|
|||||||
},
|
},
|
||||||
quantity: expenseItem.qty,
|
quantity: expenseItem.qty,
|
||||||
price: expenseItem.price,
|
price: expenseItem.price,
|
||||||
notes: expenseItem.note,
|
notes: expenseItem.notes,
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ const ExpenseRealizationForm = ({
|
|||||||
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
}, [formikSetValues, getExpenseRealizationFormInitialValues, initialValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -297,6 +297,7 @@ const ExpenseRealizationForm = ({
|
|||||||
|
|
||||||
<ExpenseKandangsTable
|
<ExpenseKandangsTable
|
||||||
type='detail'
|
type='detail'
|
||||||
|
formType='realization'
|
||||||
locationId={formik.values.location?.value}
|
locationId={formik.values.location?.value}
|
||||||
selectedKandangs={formik.values.kandangs ?? []}
|
selectedKandangs={formik.values.kandangs ?? []}
|
||||||
onChange={kandangsChangeHandler}
|
onChange={kandangsChangeHandler}
|
||||||
|
|||||||
@@ -41,22 +41,25 @@ type ExpenseFormSchemaType = {
|
|||||||
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
category: Yup.object({
|
category: Yup.object({
|
||||||
value: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
value: Yup.string()
|
||||||
label: Yup.string().oneOf(['BOP', 'NON-BOP']).required(),
|
.oneOf(['BOP', 'NON-BOP'])
|
||||||
|
.required('Kategori wajib diisi!'),
|
||||||
|
label: Yup.string()
|
||||||
|
.oneOf(['BOP', 'NON-BOP'])
|
||||||
|
.required('Kategori wajib diisi!'),
|
||||||
})
|
})
|
||||||
.nullable()
|
.nullable()
|
||||||
.optional(),
|
.required('Kategori wajib diisi!')
|
||||||
|
.typeError('Kategori wajib diisi!'),
|
||||||
|
|
||||||
location: Yup.object({
|
location: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
location_id: Yup.number()
|
location_id: Yup.number()
|
||||||
.required('Lokasi wajib diisi!')
|
|
||||||
.min(1, 'Lokasi wajib diisi!')
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
.typeError('Lokasi wajib diisi!'),
|
.typeError('Lokasi wajib diisi!'),
|
||||||
|
|
||||||
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
|
||||||
@@ -73,9 +76,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
|||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
})
|
}).nullable(),
|
||||||
.nullable()
|
|
||||||
.optional(),
|
|
||||||
|
|
||||||
supplier_id: Yup.number()
|
supplier_id: Yup.number()
|
||||||
.required('Vendor wajib diisi!')
|
.required('Vendor wajib diisi!')
|
||||||
@@ -104,9 +105,12 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
|
|||||||
.of(
|
.of(
|
||||||
Yup.object({
|
Yup.object({
|
||||||
nonstock: Yup.object({
|
nonstock: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required('Nonstock wajib diisi!'),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required('Nonstock wajib diisi!'),
|
||||||
}).nullable(),
|
})
|
||||||
|
.nullable()
|
||||||
|
.required('Nonstock wajib diisi!')
|
||||||
|
.typeError('Nonstock wajib diisi!'),
|
||||||
nonstock_id: Yup.number()
|
nonstock_id: Yup.number()
|
||||||
.required('Nonstock wajib diisi!')
|
.required('Nonstock wajib diisi!')
|
||||||
.min(1, 'Nonstock wajib diisi!')
|
.min(1, 'Nonstock wajib diisi!')
|
||||||
@@ -204,7 +208,7 @@ export const getExpenseFormInitialValues = (
|
|||||||
nonstock_id: expenseItem.nonstock.id,
|
nonstock_id: expenseItem.nonstock.id,
|
||||||
quantity: expenseItem.qty,
|
quantity: expenseItem.qty,
|
||||||
price: expenseItem.price,
|
price: expenseItem.price,
|
||||||
notes: expenseItem.note,
|
notes: expenseItem.notes,
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -190,30 +190,18 @@ const ExpenseRequestForm = ({
|
|||||||
formik.setFieldValue('category', val);
|
formik.setFieldValue('category', val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const locationChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const locationChangeHandler = useCallback(
|
||||||
formik.setFieldTouched('location', true);
|
(val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldValue('location', val);
|
const location = val as OptionType | null;
|
||||||
|
const locationId = location ? Number(location.value) : 0;
|
||||||
|
|
||||||
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('location', location);
|
||||||
|
formik.setFieldTouched('location_id', true);
|
||||||
formik.setFieldValue('kandangs', []);
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
},
|
||||||
// Auto-create expense item for location (without kandang)
|
[]
|
||||||
formik.setFieldValue('expense_nonstocks', [
|
);
|
||||||
{
|
|
||||||
cost_items: [
|
|
||||||
{
|
|
||||||
nonstock: null,
|
|
||||||
nonstock_id: 0,
|
|
||||||
quantity: undefined,
|
|
||||||
price: undefined,
|
|
||||||
notes: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const kandangsChangeHandler = (
|
const kandangsChangeHandler = (
|
||||||
kandangs: { id?: number; name?: string }[]
|
kandangs: { id?: number; name?: string }[]
|
||||||
@@ -268,6 +256,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
const supplierChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||||
formik.setFieldTouched('supplier', true);
|
formik.setFieldTouched('supplier', true);
|
||||||
|
formik.setFieldTouched('supplier_id', true);
|
||||||
formik.setFieldValue('supplier', val);
|
formik.setFieldValue('supplier', val);
|
||||||
|
|
||||||
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
const supplierId = Array.isArray(val) ? val[0]?.value : val?.value;
|
||||||
@@ -360,7 +349,7 @@ const ExpenseRequestForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className='w-full max-w-5xl'>
|
<section className='w-full'>
|
||||||
<header className='flex flex-col gap-4'>
|
<header className='flex flex-col gap-4'>
|
||||||
<Button
|
<Button
|
||||||
href='/expense'
|
href='/expense'
|
||||||
@@ -407,6 +396,16 @@ const ExpenseRequestForm = ({
|
|||||||
placeholder='Pilih Kategori'
|
placeholder='Pilih Kategori'
|
||||||
value={formik.values.category}
|
value={formik.values.category}
|
||||||
onChange={categoryChangeHandler}
|
onChange={categoryChangeHandler}
|
||||||
|
isError={
|
||||||
|
formik.touched.category && Boolean(formik.errors.category)
|
||||||
|
}
|
||||||
|
errorMessage={
|
||||||
|
formik.touched.category && formik.errors.category
|
||||||
|
? typeof formik.errors.category === 'object'
|
||||||
|
? 'Kategori wajib diisi!'
|
||||||
|
: (formik.errors.category as string)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 'BOP',
|
value: 'BOP',
|
||||||
@@ -427,8 +426,13 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
onChange={locationChangeHandler}
|
onChange={locationChangeHandler}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
isLoading={isLoadingLocationOptions}
|
|
||||||
onInputChange={setLocationInputValue}
|
onInputChange={setLocationInputValue}
|
||||||
|
isLoading={isLoadingLocationOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.location_id as string}
|
||||||
|
isClearable
|
||||||
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
className={{ wrapper: 'col-span-12 sm:col-span-4' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -438,6 +442,12 @@ const ExpenseRequestForm = ({
|
|||||||
required
|
required
|
||||||
value={formik.values.transaction_date}
|
value={formik.values.transaction_date}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
isError={
|
||||||
|
formik.touched.transaction_date &&
|
||||||
|
Boolean(formik.errors.transaction_date)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.transaction_date as string}
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-4',
|
wrapper: 'col-span-12 sm:col-span-4',
|
||||||
}}
|
}}
|
||||||
@@ -460,8 +470,12 @@ const ExpenseRequestForm = ({
|
|||||||
value={formik.values.supplier}
|
value={formik.values.supplier}
|
||||||
onChange={supplierChangeHandler}
|
onChange={supplierChangeHandler}
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
isLoading={isLoadingVendorOptions}
|
|
||||||
onInputChange={setVendorInputValue}
|
onInputChange={setVendorInputValue}
|
||||||
|
isLoading={isLoadingVendorOptions}
|
||||||
|
isError={
|
||||||
|
formik.touched.supplier_id && Boolean(formik.errors.supplier_id)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.supplier_id as string}
|
||||||
className={{ wrapper: 'col-span-12' }}
|
className={{ wrapper: 'col-span-12' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
`expense_nonstocks[${kandangExpenseIdx}].cost_items[${expenseIdx}].nonstock`,
|
||||||
val
|
val
|
||||||
@@ -96,7 +100,7 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isExpenseRepeaterInputError = (
|
const isExpenseRepeaterInputError = (
|
||||||
column: 'nonstock' | 'quantity' | 'price' | 'notes',
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
kandangExpenseIdx: number,
|
kandangExpenseIdx: number,
|
||||||
expenseIdx: number
|
expenseIdx: number
|
||||||
) => {
|
) => {
|
||||||
@@ -105,11 +109,14 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column] &&
|
]?.[column] &&
|
||||||
Boolean(
|
Boolean(
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx] instanceof
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx] &&
|
||||||
Object &&
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx] ===
|
||||||
|
'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
] instanceof Object &&
|
] &&
|
||||||
|
typeof formik.errors.expense_nonstocks?.[kandangExpenseIdx]
|
||||||
|
.cost_items?.[expenseIdx] === 'object' &&
|
||||||
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
formik.errors.expense_nonstocks?.[kandangExpenseIdx].cost_items?.[
|
||||||
expenseIdx
|
expenseIdx
|
||||||
]?.[column]
|
]?.[column]
|
||||||
@@ -117,6 +124,32 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getExpenseRepeaterErrorMessage = (
|
||||||
|
column: 'nonstock_id' | 'quantity' | 'price' | 'notes',
|
||||||
|
kandangExpenseIdx: number,
|
||||||
|
expenseIdx: number
|
||||||
|
): string => {
|
||||||
|
const kandangError = formik.errors.expense_nonstocks?.[kandangExpenseIdx];
|
||||||
|
|
||||||
|
if (!kandangError || typeof kandangError !== 'object') return '';
|
||||||
|
|
||||||
|
if (!('cost_items' in kandangError)) return '';
|
||||||
|
|
||||||
|
const costItemsError = kandangError.cost_items?.[expenseIdx];
|
||||||
|
|
||||||
|
if (!costItemsError || typeof costItemsError !== 'object') return '';
|
||||||
|
|
||||||
|
const fieldError = costItemsError[column as keyof typeof costItemsError];
|
||||||
|
|
||||||
|
if (!fieldError) return '';
|
||||||
|
|
||||||
|
if (typeof fieldError === 'object' && fieldError !== null) {
|
||||||
|
return 'Nonstock wajib diisi!';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(fieldError);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={{
|
className={{
|
||||||
@@ -202,10 +235,21 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
val
|
val
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
isError={isExpenseRepeaterInputError(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'nonstock_id',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
options={nonstockOptions}
|
options={nonstockOptions}
|
||||||
isLoading={isLoadingNonstockOptions}
|
isLoading={isLoadingNonstockOptions}
|
||||||
onInputChange={setNonstockInputValue}
|
onInputChange={setNonstockInputValue}
|
||||||
className={{ wrapper: 'min-w-48' }}
|
className={{ wrapper: 'min-w-48' }}
|
||||||
|
isClearable={true}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -226,6 +270,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'quantity',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
@@ -246,6 +295,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'price',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
inputPrefix={
|
inputPrefix={
|
||||||
<span className='text-gray-600 font-medium'>
|
<span className='text-gray-600 font-medium'>
|
||||||
Rp
|
Rp
|
||||||
@@ -271,6 +325,11 @@ const ExpenseRequestKandangDetailExpense: React.FC<
|
|||||||
kandangExpenseIdx,
|
kandangExpenseIdx,
|
||||||
expenseIdx
|
expenseIdx
|
||||||
)}
|
)}
|
||||||
|
errorMessage={getExpenseRepeaterErrorMessage(
|
||||||
|
'notes',
|
||||||
|
kandangExpenseIdx,
|
||||||
|
expenseIdx
|
||||||
|
)}
|
||||||
className={{ wrapper: 'min-w-24' }}
|
className={{ wrapper: 'min-w-24' }}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
{pengajuan.note}
|
{pengajuan.notes}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -607,7 +607,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
<Text style={ExpensePDFStyle.kandangExpenseLabelText}>
|
||||||
{realisasi.note}
|
{realisasi.notes}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Pihak',
|
label: 'Pihak',
|
||||||
value: finance.party.id ? finance.party.name : '-',
|
value: finance.party?.id ? finance.party?.name : '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Tanggal',
|
label: 'Tanggal',
|
||||||
@@ -56,25 +56,21 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nomor Rekening',
|
label: 'Nomor Rekening',
|
||||||
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
value: `${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `Rekening ${formatTitleCase(finance.party.type)}`,
|
label: `Rekening ${formatTitleCase(finance.party?.type)}`,
|
||||||
value: finance.party.account_number,
|
value: finance.party?.account_number,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Nominal',
|
label: 'Nominal',
|
||||||
value: formatCurrency(finance.expense_amount),
|
value: formatCurrency(finance.nominal),
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Sisa',
|
|
||||||
value: formatCurrency(finance.income_amount),
|
|
||||||
},
|
},
|
||||||
].filter((item) => {
|
].filter((item) => {
|
||||||
// Hide party account number row if transaction type is INJECTION
|
// Hide party account number row if transaction type is INJECTION
|
||||||
if (
|
if (
|
||||||
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
|
FINANCE_INJECTION_STATUS.includes(finance.transaction_type) &&
|
||||||
item.label === `Rekening ${formatTitleCase(finance.party.type)}`
|
item.label === `Rekening ${formatTitleCase(finance.party?.type)}`
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -148,18 +144,19 @@ const FinanceDetail = ({ finance }: { finance: Finance }) => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className='flex flex-row gap-2 justify-end'>
|
<div className='flex flex-row gap-2 justify-end'>
|
||||||
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
|
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) &&
|
||||||
<RequirePermission permissions='lti.finance.payments.update'>
|
finance.party?.type !== 'SUPPLIER' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
color='warning'
|
<Button
|
||||||
className='min-w-24'
|
color='warning'
|
||||||
href={`/finance/detail/edit?financeId=${finance.id}`}
|
className='min-w-24'
|
||||||
>
|
href={`/finance/detail/edit?financeId=${finance.id}`}
|
||||||
<Icon icon='mdi:pencil-outline' />
|
>
|
||||||
Edit
|
<Icon icon='mdi:pencil-outline' />
|
||||||
</Button>
|
Edit
|
||||||
</RequirePermission>
|
</Button>
|
||||||
)}
|
</RequirePermission>
|
||||||
|
)}
|
||||||
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
|
||||||
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
<RequirePermission permissions='lti.finance.initial_balances.update'>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { ChangeEventHandler, useMemo, useState } from 'react';
|
import { ChangeEventHandler, useMemo, useState } from 'react';
|
||||||
import { CellContext, Row } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import Dropdown from '@/components/dropdown/Dropdown';
|
|
||||||
import DateInput from '@/components/input/DateInput';
|
import DateInput from '@/components/input/DateInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
import SelectInput, {
|
import SelectInput, {
|
||||||
OptionType,
|
OptionType,
|
||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import Menu from '@/components/menu/Menu';
|
|
||||||
import MenuItem from '@/components/menu/MenuItem';
|
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import Tooltip from '@/components/Tooltip';
|
|
||||||
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { Finance } from '@/types/api/finance/finance';
|
import { Finance } from '@/types/api/finance/finance';
|
||||||
@@ -23,7 +19,6 @@ import {
|
|||||||
FINANCE_INITIAL_BALANCE_STATUS,
|
FINANCE_INITIAL_BALANCE_STATUS,
|
||||||
FINANCE_INJECTION_STATUS,
|
FINANCE_INJECTION_STATUS,
|
||||||
FINANCE_TRANSACTION_STATUS,
|
FINANCE_TRANSACTION_STATUS,
|
||||||
ROWS_OPTIONS,
|
|
||||||
} from '@/config/constant';
|
} from '@/config/constant';
|
||||||
import { FinanceApi } from '@/services/api/finance';
|
import { FinanceApi } from '@/services/api/finance';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseSuccess } from '@/lib/api-helper';
|
||||||
@@ -70,19 +65,24 @@ const RowOptionsMenu = ({
|
|||||||
|
|
||||||
{FINANCE_TRANSACTION_STATUS.includes(
|
{FINANCE_TRANSACTION_STATUS.includes(
|
||||||
props.row.original.transaction_type
|
props.row.original.transaction_type
|
||||||
) && (
|
) &&
|
||||||
<RequirePermission permissions='lti.finance.payments.update'>
|
props.row.original.party?.type !== 'SUPPLIER' && (
|
||||||
<Button
|
<RequirePermission permissions='lti.finance.payments.update'>
|
||||||
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
<Button
|
||||||
variant='ghost'
|
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
|
||||||
color='warning'
|
variant='ghost'
|
||||||
className='justify-start text-sm'
|
color='warning'
|
||||||
>
|
className='justify-start text-sm'
|
||||||
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
>
|
||||||
Edit
|
<Icon
|
||||||
</Button>
|
icon='material-symbols:edit-outline'
|
||||||
</RequirePermission>
|
width={16}
|
||||||
)}
|
height={16}
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
|
)}
|
||||||
|
|
||||||
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
{FINANCE_INITIAL_BALANCE_STATUS.includes(
|
||||||
props.row.original.transaction_type
|
props.row.original.transaction_type
|
||||||
@@ -199,35 +199,37 @@ const FinanceTable = () => {
|
|||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const transactionTypeOptions = useMemo(() => {
|
const transactionTypeOptions = useMemo(() => {
|
||||||
return [
|
|
||||||
{ label: 'Transfer', value: 'TRANSFER' },
|
|
||||||
{ label: 'Cash', value: 'CASH' },
|
|
||||||
{ label: 'Card', value: 'CARD' },
|
|
||||||
{ label: 'Cheque', value: 'CHEQUE' },
|
|
||||||
{ label: 'Saldo', value: 'SALDO' },
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
const partyTypeOptions = useMemo(() => {
|
|
||||||
return [
|
return [
|
||||||
{ label: 'Customer', value: 'CUSTOMER' },
|
{ label: 'Customer', value: 'CUSTOMER' },
|
||||||
{ label: 'Supplier', value: 'SUPPLIER' },
|
{ label: 'Supplier', value: 'SUPPLIER' },
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
const {
|
||||||
|
options: partyTypeOptions,
|
||||||
|
isLoadingOptions: partyTypeIsLoadingOptions,
|
||||||
|
setInputValue: partyTypeInputValue,
|
||||||
|
loadMore: partyTypeLoadMore,
|
||||||
|
} = useSelect(
|
||||||
|
selectedTransactionType
|
||||||
|
? selectedTransactionType.value === 'CUSTOMER'
|
||||||
|
? CustomerApi.basePath
|
||||||
|
: SupplierApi.basePath
|
||||||
|
: '',
|
||||||
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
const sortByOptions = useMemo(() => {
|
const sortByOptions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
|
||||||
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
{ label: 'Tanggal Dibuat', value: 'created_at' },
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
|
const {
|
||||||
BankApi.basePath,
|
options: bankOptions,
|
||||||
'id',
|
rawData: bankRawData,
|
||||||
'alias',
|
setInputValue: bankInputValue,
|
||||||
'',
|
loadMore: bankLoadMore,
|
||||||
{
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'alias');
|
||||||
limit: 'limit',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Handler =====
|
// ===== Handler =====
|
||||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
@@ -344,10 +346,10 @@ const FinanceTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pihak',
|
header: 'Pihak',
|
||||||
accessorFn: (finance: Finance) => finance.party.name,
|
accessorFn: (finance: Finance) => finance.party?.name,
|
||||||
cell: (props: CellContext<Finance, unknown>) => {
|
cell: (props: CellContext<Finance, unknown>) => {
|
||||||
if (props.row.original.party.id) {
|
if (props.row.original.party?.id) {
|
||||||
return <span>{props.row.original.party.name}</span>;
|
return <span>{props.row.original.party?.name}</span>;
|
||||||
}
|
}
|
||||||
return <span>{'-'}</span>;
|
return <span>{'-'}</span>;
|
||||||
},
|
},
|
||||||
@@ -368,12 +370,12 @@ const FinanceTable = () => {
|
|||||||
{
|
{
|
||||||
header: 'Bank',
|
header: 'Bank',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
|
`${finance.bank?.alias} - ${finance.bank?.account_number} - ${finance.bank?.owner}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pengeluaran (Rp)',
|
header: 'Pengeluaran (Rp)',
|
||||||
accessorFn: (finance: Finance) =>
|
accessorFn: (finance: Finance) =>
|
||||||
formatCurrency(finance.expense_amount),
|
formatCurrency(Math.abs(finance.expense_amount)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Pemasukan (Rp)',
|
header: 'Pemasukan (Rp)',
|
||||||
@@ -476,38 +478,49 @@ const FinanceTable = () => {
|
|||||||
<div className='grid grid-cols-4 gap-6'>
|
<div className='grid grid-cols-4 gap-6'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={transactionTypeOptions}
|
options={transactionTypeOptions}
|
||||||
label='Jenis Transaksi'
|
label='Tipe Transaksi'
|
||||||
value={selectedTransactionType}
|
value={selectedTransactionType}
|
||||||
onChange={transactionTypeChangeHandler}
|
onChange={transactionTypeChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
|
<SelectInput
|
||||||
|
options={partyTypeOptions}
|
||||||
|
label={
|
||||||
|
selectedTransactionType
|
||||||
|
? selectedTransactionType.value === 'CUSTOMER'
|
||||||
|
? 'Pelanggan'
|
||||||
|
: 'Supplier'
|
||||||
|
: 'Pihak'
|
||||||
|
}
|
||||||
|
value={selectedPartyType}
|
||||||
|
onChange={partyTypeChangeHandler}
|
||||||
|
onInputChange={partyTypeInputValue}
|
||||||
|
onMenuScrollToBottom={partyTypeLoadMore}
|
||||||
|
isLoading={partyTypeIsLoadingOptions}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={
|
options={
|
||||||
isResponseSuccess(bankRawData)
|
isResponseSuccess(bankRawData)
|
||||||
? bankOptions.map((bank) => ({
|
? bankOptions.map((bank) => ({
|
||||||
label:
|
label:
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.alias +
|
?.alias +
|
||||||
' - ' +
|
' - ' +
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.account_number +
|
?.account_number +
|
||||||
' - ' +
|
' - ' +
|
||||||
bankRawData.data.find((data) => data.id === bank.value)
|
bankRawData.data.find((data) => data.id === bank?.value)
|
||||||
?.owner,
|
?.owner,
|
||||||
value: bank.value,
|
value: bank?.value,
|
||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
label='Bank'
|
label='Bank'
|
||||||
value={selectedBank}
|
value={selectedBank}
|
||||||
onChange={bankChangeHandler}
|
onChange={bankChangeHandler}
|
||||||
isClearable
|
onInputChange={bankInputValue}
|
||||||
/>
|
onMenuScrollToBottom={bankLoadMore}
|
||||||
<SelectInput
|
|
||||||
options={partyTypeOptions}
|
|
||||||
label='Pihak'
|
|
||||||
value={selectedPartyType}
|
|
||||||
onChange={partyTypeChangeHandler}
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -32,8 +32,10 @@ import {
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface FormFinanceAddProps {
|
interface FormFinanceAddProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -51,18 +53,22 @@ const FormFinanceAdd = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceAddProps) => {
|
}: FormFinanceAddProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
const [isSupplier, setIsSupplier] = useState(
|
||||||
|
initialValues?.party?.type === 'SUPPLIER'
|
||||||
|
);
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): FinanceFormValues => {
|
const formikInitialValues = useMemo((): FinanceFormValues => {
|
||||||
return {
|
return {
|
||||||
party_type_option:
|
party_type_option:
|
||||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
(option) => option.value === initialValues?.party.type
|
(option) => option.value === initialValues?.party?.type
|
||||||
) || null,
|
) || null,
|
||||||
party_id_option: initialValues?.party
|
party_id_option: initialValues?.party
|
||||||
? {
|
? {
|
||||||
label: initialValues?.party.name || '',
|
label: initialValues?.party?.name || '',
|
||||||
value: initialValues?.party.id || 0,
|
value: initialValues?.party?.id || 0,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
payment_date: initialValues?.payment_date || '',
|
payment_date: initialValues?.payment_date || '',
|
||||||
@@ -72,11 +78,11 @@ const FormFinanceAdd = ({
|
|||||||
) || null,
|
) || null,
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues?.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues?.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
party_account_number: initialValues?.party.account_number || '',
|
party_account_number: initialValues?.party?.account_number || '',
|
||||||
reference_number: initialValues?.reference_number || '',
|
reference_number: initialValues?.reference_number || '',
|
||||||
nominal: initialValues?.nominal.toString() || '',
|
nominal: initialValues?.nominal.toString() || '',
|
||||||
notes: initialValues?.notes || '',
|
notes: initialValues?.notes || '',
|
||||||
@@ -113,20 +119,22 @@ const FormFinanceAdd = ({
|
|||||||
options: partyOptions,
|
options: partyOptions,
|
||||||
isLoadingOptions: isLoadingPartyOptions,
|
isLoadingOptions: isLoadingPartyOptions,
|
||||||
rawData: partyRawData,
|
rawData: partyRawData,
|
||||||
|
setInputValue: setPartyInputValue,
|
||||||
|
loadMore: loadMorePartyOptions,
|
||||||
} = useSelect<PartyCommonProps>(
|
} = useSelect<PartyCommonProps>(
|
||||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
? CustomerApi.basePath
|
? CustomerApi.basePath
|
||||||
: SupplierApi.basePath,
|
: SupplierApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'name',
|
'name'
|
||||||
'',
|
|
||||||
{ limit: 'limit' }
|
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -151,6 +159,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +175,7 @@ const FormFinanceAdd = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +215,7 @@ const FormFinanceAdd = ({
|
|||||||
? formik.errors.party_type_option
|
? formik.errors.party_type_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
isDisabled={type === 'edit' || isSupplier}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
@@ -219,6 +230,8 @@ const FormFinanceAdd = ({
|
|||||||
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
|
||||||
options={partyOptions}
|
options={partyOptions}
|
||||||
value={formik.values.party_id_option}
|
value={formik.values.party_id_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_id_option', value);
|
formik.setFieldValue('party_id_option', value);
|
||||||
if (isResponseSuccess(partyRawData) && value) {
|
if (isResponseSuccess(partyRawData) && value) {
|
||||||
@@ -241,7 +254,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={!formik.values.party_type_option?.value}
|
isDisabled={!formik.values.party_type_option?.value || isSupplier}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
label='Tanggal'
|
label='Tanggal'
|
||||||
@@ -259,6 +272,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Metode Pembayaran'
|
label='Metode Pembayaran'
|
||||||
@@ -280,6 +294,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Bank'
|
label='Bank'
|
||||||
@@ -304,6 +319,8 @@ const FormFinanceAdd = ({
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
value={formik.values.bank_id_option}
|
value={formik.values.bank_id_option}
|
||||||
|
onInputChange={setBankInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreBankOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('bank_id_option', value);
|
formik.setFieldValue('bank_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -318,6 +335,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
isClearable
|
isClearable
|
||||||
|
isDisabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
|
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
|
||||||
@@ -338,6 +356,7 @@ const FormFinanceAdd = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
readOnly
|
readOnly
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='Nomor Referensi'
|
label='Nomor Referensi'
|
||||||
@@ -357,6 +376,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
label='Nominal'
|
label='Nominal'
|
||||||
@@ -372,6 +392,7 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
label='Catatan'
|
label='Catatan'
|
||||||
@@ -387,8 +408,18 @@ const FormFinanceAdd = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
disabled={isSupplier}
|
||||||
/>
|
/>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
+1
-7
@@ -27,13 +27,7 @@ export const InitialBalanceFormSchema = Yup.object().shape({
|
|||||||
'Pihak wajib diisi',
|
'Pihak wajib diisi',
|
||||||
(value) => value !== null && value !== undefined
|
(value) => value !== null && value !== undefined
|
||||||
),
|
),
|
||||||
bank_id_option: Yup.mixed()
|
bank_id_option: Yup.mixed().nullable(),
|
||||||
.nullable()
|
|
||||||
.test(
|
|
||||||
'is-valid-option',
|
|
||||||
'Bank wajib diisi',
|
|
||||||
(value) => value !== null && value !== undefined
|
|
||||||
),
|
|
||||||
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
|
||||||
initial_balance_type_option: Yup.mixed()
|
initial_balance_type_option: Yup.mixed()
|
||||||
.nullable()
|
.nullable()
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ import { Bank } from '@/types/api/master-data/bank';
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
|
||||||
interface FormFinanceAddInitialBalanceProps {
|
interface FormFinanceAddInitialBalanceProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -42,6 +43,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceAddInitialBalanceProps) => {
|
}: FormFinanceAddInitialBalanceProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
|
||||||
@@ -53,18 +55,18 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
return {
|
return {
|
||||||
party_type_option:
|
party_type_option:
|
||||||
FINANCE_PARTY_TYPE_OPTIONS.find(
|
FINANCE_PARTY_TYPE_OPTIONS.find(
|
||||||
(option) => option.value === initialValues?.party.type
|
(option) => option.value === initialValues?.party?.type
|
||||||
) || null,
|
) || null,
|
||||||
party_id_option: initialValues?.party
|
party_id_option: initialValues?.party
|
||||||
? {
|
? {
|
||||||
label: initialValues.party.name,
|
label: initialValues.party?.name,
|
||||||
value: initialValues.party.id,
|
value: initialValues.party?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
reference_number: initialValues?.reference_number || '',
|
reference_number: initialValues?.reference_number || '',
|
||||||
@@ -104,21 +106,25 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== Options =====
|
// ===== Options =====
|
||||||
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
|
const {
|
||||||
useSelect(
|
options: partyOptions,
|
||||||
formik.values.party_type_option?.value === 'CUSTOMER'
|
isLoadingOptions: isLoadingPartyOptions,
|
||||||
? CustomerApi.basePath
|
setInputValue: setPartyInputValue,
|
||||||
: SupplierApi.basePath,
|
loadMore: loadMorePartyOptions,
|
||||||
'id',
|
} = useSelect(
|
||||||
'name',
|
formik.values.party_type_option?.value === 'CUSTOMER'
|
||||||
'',
|
? CustomerApi.basePath
|
||||||
{ limit: 'limit' }
|
: SupplierApi.basePath,
|
||||||
);
|
'id',
|
||||||
|
'name'
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -143,6 +149,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +169,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +197,8 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
placeholder='Pilih jenis pihak'
|
placeholder='Pilih jenis pihak'
|
||||||
options={FINANCE_PARTY_TYPE_OPTIONS}
|
options={FINANCE_PARTY_TYPE_OPTIONS}
|
||||||
value={formik.values.party_type_option}
|
value={formik.values.party_type_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_type_option', value);
|
formik.setFieldValue('party_type_option', value);
|
||||||
formik.setFieldValue('party_id_option', null);
|
formik.setFieldValue('party_id_option', null);
|
||||||
@@ -205,6 +215,7 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
|
isDisabled={type === 'edit'}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -218,6 +229,8 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
|
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
|
||||||
options={partyOptions}
|
options={partyOptions}
|
||||||
value={formik.values.party_id_option}
|
value={formik.values.party_id_option}
|
||||||
|
onInputChange={setPartyInputValue}
|
||||||
|
onMenuScrollToBottom={loadMorePartyOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('party_id_option', value);
|
formik.setFieldValue('party_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -269,7 +282,6 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
? formik.errors.bank_id_option
|
? formik.errors.bank_id_option
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
required
|
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -354,7 +366,18 @@ const FormFinanceAddInitialBalance = ({
|
|||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ import {
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
import { Icon } from '@iconify/react';
|
||||||
|
|
||||||
interface FormFinanceInjectionProps {
|
interface FormFinanceInjectionProps {
|
||||||
type?: 'add' | 'edit';
|
type?: 'add' | 'edit';
|
||||||
@@ -37,14 +39,15 @@ const FormFinanceInjection = ({
|
|||||||
initialValues,
|
initialValues,
|
||||||
}: FormFinanceInjectionProps) => {
|
}: FormFinanceInjectionProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState('');
|
||||||
|
|
||||||
// ===== Formik =====
|
// ===== Formik =====
|
||||||
const formikInitialValues = useMemo((): InjectionFormValues => {
|
const formikInitialValues = useMemo((): InjectionFormValues => {
|
||||||
return {
|
return {
|
||||||
bank_id_option: initialValues?.bank
|
bank_id_option: initialValues?.bank
|
||||||
? {
|
? {
|
||||||
label: initialValues.bank.name,
|
label: initialValues.bank?.name,
|
||||||
value: initialValues.bank.id,
|
value: initialValues.bank?.id,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
adjustment_date: initialValues?.payment_date || '',
|
adjustment_date: initialValues?.payment_date || '',
|
||||||
@@ -80,7 +83,9 @@ const FormFinanceInjection = ({
|
|||||||
options: bankOptions,
|
options: bankOptions,
|
||||||
rawData: bankRawData,
|
rawData: bankRawData,
|
||||||
isLoadingOptions: isLoadingBankOptions,
|
isLoadingOptions: isLoadingBankOptions,
|
||||||
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
|
setInputValue: setBankInputValue,
|
||||||
|
loadMore: loadMoreBankOptions,
|
||||||
|
} = useSelect<Bank>(BankApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ===== Helper Functions =====
|
// ===== Helper Functions =====
|
||||||
const transformFormValuesToPayload = (
|
const transformFormValuesToPayload = (
|
||||||
@@ -101,6 +106,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +123,7 @@ const FormFinanceInjection = ({
|
|||||||
|
|
||||||
if (isResponseError(response)) {
|
if (isResponseError(response)) {
|
||||||
toast.error(response.message);
|
toast.error(response.message);
|
||||||
|
setServerErrorMessage(response.message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +169,8 @@ const FormFinanceInjection = ({
|
|||||||
: []
|
: []
|
||||||
}
|
}
|
||||||
value={formik.values.bank_id_option}
|
value={formik.values.bank_id_option}
|
||||||
|
onInputChange={setBankInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreBankOptions}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
formik.setFieldValue('bank_id_option', value);
|
formik.setFieldValue('bank_id_option', value);
|
||||||
}}
|
}}
|
||||||
@@ -226,6 +235,15 @@ const FormFinanceInjection = ({
|
|||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<Alert color='error'>
|
||||||
|
<Icon icon='mdi:alert' />
|
||||||
|
{serverErrorMessage}
|
||||||
|
<Button color='error' onClick={() => setServerErrorMessage('')}>
|
||||||
|
<Icon icon='mdi:close' />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
<div className='flex justify-center gap-4'>
|
<div className='flex justify-center gap-4'>
|
||||||
<Button
|
<Button
|
||||||
type='reset'
|
type='reset'
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,14 @@ const DeliveryProductObjectSchema = Yup.object({
|
|||||||
.typeError('Qty harus berupa angka!'),
|
.typeError('Qty harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DeliveryDocumentSchema = Yup.mixed<File | MovementDocument>()
|
||||||
|
.nullable()
|
||||||
|
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value): boolean => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
||||||
delivery_cost: Yup.number()
|
delivery_cost: Yup.number()
|
||||||
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
.transform((value) => (isNaN(value) || value === 0 ? undefined : value))
|
||||||
@@ -135,13 +143,7 @@ const DeliveryObjectSchema: Yup.ObjectSchema<DeliverySchema> = Yup.object({
|
|||||||
}),
|
}),
|
||||||
document_path: Yup.string().nullable().optional(),
|
document_path: Yup.string().nullable().optional(),
|
||||||
document_index: Yup.number().optional(),
|
document_index: Yup.number().optional(),
|
||||||
document: Yup.mixed<File | MovementDocument>()
|
document: DeliveryDocumentSchema,
|
||||||
.nullable()
|
|
||||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
|
||||||
if (!value) return true;
|
|
||||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
driver_name: Yup.string().required('Nama sopir wajib diisi!'),
|
||||||
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
vehicle_plate: Yup.string().required('Plat nomor wajib diisi!'),
|
||||||
supplier: Yup.object({
|
supplier: Yup.object({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -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,13 @@ 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', {
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
|
});
|
||||||
|
|
||||||
// ===== SELECT INPUT DATA =====
|
// ===== SELECT INPUT DATA =====
|
||||||
const {
|
const {
|
||||||
@@ -107,12 +108,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();
|
||||||
@@ -268,26 +263,64 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
const prevSourceWarehouseIdRef = useRef<number | null>(
|
||||||
const getProductWarehousesUrl = useCallback(() => {
|
formik.values.source_warehouse_id
|
||||||
const productWarehouseParams = new URLSearchParams({
|
);
|
||||||
search: productWarehouseSelectInputValue,
|
|
||||||
});
|
|
||||||
if (formik.values.source_warehouse_id) {
|
|
||||||
productWarehouseParams.append(
|
|
||||||
'warehouse_id',
|
|
||||||
formik.values.source_warehouse_id.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return `${ProductWarehouseApi.basePath}?${productWarehouseParams.toString()}`;
|
|
||||||
}, [formik.values.source_warehouse_id, productWarehouseSelectInputValue]);
|
|
||||||
|
|
||||||
const productWarehousesUrl = getProductWarehousesUrl();
|
// ===== RESET PRODUCTS WHEN SOURCE WAREHOUSE CHANGES =====
|
||||||
const { data: productWarehouses, isLoading: isLoadingProductWarehouses } =
|
useEffect(() => {
|
||||||
useSWR(
|
const prevSourceWarehouseId = prevSourceWarehouseIdRef.current;
|
||||||
formik.values.source_warehouse_id ? productWarehousesUrl : null,
|
const currentSourceWarehouseId = formik.values.source_warehouse_id;
|
||||||
ProductWarehouseApi.getAllFetcher
|
|
||||||
);
|
if (
|
||||||
|
prevSourceWarehouseId !== currentSourceWarehouseId &&
|
||||||
|
prevSourceWarehouseId !== null
|
||||||
|
) {
|
||||||
|
formik.setFieldValue('products', [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
formik.setFieldTouched('products', false);
|
||||||
|
|
||||||
|
const updatedDeliveries = formik.values.deliveries.map(
|
||||||
|
(delivery: DeliverySchema) => ({
|
||||||
|
...delivery,
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
product: null,
|
||||||
|
product_id: 0,
|
||||||
|
product_qty: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
|
formik.setFieldTouched('deliveries', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSourceWarehouseIdRef.current = currentSourceWarehouseId;
|
||||||
|
}, [formik.values.source_warehouse_id, formik.values.deliveries]);
|
||||||
|
|
||||||
|
// ===== PRODUCT WAREHOUSE FETCHING (after form initialization) =====
|
||||||
|
const {
|
||||||
|
setInputValue: setProductWarehouseSelectInputValue,
|
||||||
|
isLoadingOptions: isLoadingProductWarehouses,
|
||||||
|
loadMore: loadMoreProductWarehouses,
|
||||||
|
rawData: productWarehouses,
|
||||||
|
} = useSelect<ProductWarehouse>(
|
||||||
|
formik.values.source_warehouse_id ? ProductWarehouseApi.basePath : null,
|
||||||
|
'id',
|
||||||
|
'name',
|
||||||
|
'search',
|
||||||
|
{
|
||||||
|
warehouse_id: formik.values.source_warehouse_id
|
||||||
|
? formik.values.source_warehouse_id.toString()
|
||||||
|
: '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
const productWarehouseOptions = isResponseSuccess(productWarehouses)
|
||||||
? productWarehouses?.data.map((pw) => ({
|
? productWarehouses?.data.map((pw) => ({
|
||||||
@@ -357,13 +390,71 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
formik.setFieldValue('transfer_date', e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
// Product Handlers
|
const handleTransferDateChange = useCallback(
|
||||||
const addProduct = () => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
formik.setFieldValue('transfer_date', e.target.value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSourceWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newSourceWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newSourceWarehouseId &&
|
||||||
|
newSourceWarehouseId === formik.values.destination_warehouse_id
|
||||||
|
) {
|
||||||
|
const destinationWarehouseName =
|
||||||
|
(formik.values.destination_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang tujuan';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('source_warehouse', true);
|
||||||
|
formik.setFieldValue('source_warehouse', val);
|
||||||
|
formik.setFieldTouched('source_warehouse_id', true);
|
||||||
|
formik.setFieldValue('source_warehouse_id', newSourceWarehouseId);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
formik.values.destination_warehouse_id,
|
||||||
|
formik.values.destination_warehouse,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDestinationWarehouseChange = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
const newDestinationWarehouseId = (val as WarehouseOptionType)?.value;
|
||||||
|
|
||||||
|
if (
|
||||||
|
newDestinationWarehouseId &&
|
||||||
|
newDestinationWarehouseId === formik.values.source_warehouse_id
|
||||||
|
) {
|
||||||
|
const sourceWarehouseName =
|
||||||
|
(formik.values.source_warehouse as WarehouseOptionType)?.label ||
|
||||||
|
'gudang asal';
|
||||||
|
toast.error(
|
||||||
|
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formik.setFieldTouched('destination_warehouse', true);
|
||||||
|
formik.setFieldValue('destination_warehouse', val);
|
||||||
|
formik.setFieldTouched('destination_warehouse_id', true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
'destination_warehouse_id',
|
||||||
|
newDestinationWarehouseId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[formik.values.source_warehouse_id, formik.values.source_warehouse]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addProduct = useCallback(() => {
|
||||||
const newProducts = [
|
const newProducts = [
|
||||||
...(formik.values.products || []),
|
...(formik.values.products || []),
|
||||||
{
|
{
|
||||||
@@ -373,22 +464,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
formik.setFieldValue('products', newProducts);
|
formik.setFieldValue('products', newProducts);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeProduct = useCallback(
|
const removeProduct = useCallback((i: number) => {
|
||||||
(i: number) => {
|
const updatedProducts =
|
||||||
const updatedProducts =
|
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
||||||
formik.values.products?.reduce((acc: ProductSchema[], item, index) => {
|
if (index !== i) {
|
||||||
if (index !== i) {
|
acc.push(item);
|
||||||
acc.push(item);
|
}
|
||||||
}
|
return acc;
|
||||||
return acc;
|
}, []) ?? [];
|
||||||
}, []) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveProduct = useCallback(() => {
|
const bulkRemoveProduct = useCallback(() => {
|
||||||
const updatedProducts =
|
const updatedProducts =
|
||||||
@@ -397,10 +485,45 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('products', updatedProducts);
|
formik.setFieldValue('products', updatedProducts);
|
||||||
setSelectedProducts([]);
|
setSelectedProducts([]);
|
||||||
}, [formik, selectedProducts]);
|
}, [formik, selectedProducts, setSelectedProducts]);
|
||||||
|
|
||||||
// Delivery Handlers
|
const handleProductChange = useCallback(
|
||||||
const addDelivery = () => {
|
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(`products.${idx}.product`, true);
|
||||||
|
formik.setFieldValue(`products.${idx}.product`, val);
|
||||||
|
formik.setFieldTouched(`products.${idx}.product_id`, true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`products.${idx}.product_id`,
|
||||||
|
(val as ProductWarehouseOptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductSelectAllChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts(formik.values.products?.map((_, idx) => idx) ?? []);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts([]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formik.values.products, setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProductCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const idx = Number(e.target.name.replace('product-', ''));
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedProducts((prev) => [...prev, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedProducts((prev) => prev.filter((i) => i !== idx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedProducts]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addDelivery = useCallback(() => {
|
||||||
formik.setFieldValue('deliveries', [
|
formik.setFieldValue('deliveries', [
|
||||||
...(formik.values.deliveries || []),
|
...(formik.values.deliveries || []),
|
||||||
{
|
{
|
||||||
@@ -420,25 +543,19 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeDelivery = useCallback(
|
const removeDelivery = useCallback((i: number) => {
|
||||||
(i: number) => {
|
const updatedDeliveries =
|
||||||
const updatedDeliveries =
|
formik.values.deliveries?.reduce((acc: DeliverySchema[], item, index) => {
|
||||||
formik.values.deliveries?.reduce(
|
if (index !== i) {
|
||||||
(acc: DeliverySchema[], item, index) => {
|
acc.push(item);
|
||||||
if (index !== i) {
|
}
|
||||||
acc.push(item);
|
return acc;
|
||||||
}
|
}, []) ?? [];
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
},
|
}, []);
|
||||||
[formik]
|
|
||||||
);
|
|
||||||
|
|
||||||
const bulkRemoveDelivery = useCallback(() => {
|
const bulkRemoveDelivery = useCallback(() => {
|
||||||
const updatedDeliveries =
|
const updatedDeliveries =
|
||||||
@@ -447,33 +564,101 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
) ?? [];
|
) ?? [];
|
||||||
formik.setFieldValue('deliveries', updatedDeliveries);
|
formik.setFieldValue('deliveries', updatedDeliveries);
|
||||||
setSelectedDeliveries([]);
|
setSelectedDeliveries([]);
|
||||||
}, [formik, selectedDeliveries]);
|
}, [formik, selectedDeliveries, setSelectedDeliveries]);
|
||||||
|
|
||||||
// Cost Calculation Handlers
|
const handleDeliverySelectAllChange = useCallback(
|
||||||
const handleDeliveryCostChange = useCallback(
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
(idx: number, value: number) => {
|
if (e.target.checked) {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
setSelectedDeliveries(
|
||||||
|
formik.values.deliveries?.map((_, idx) => idx) ?? []
|
||||||
const delivery = formik.values.deliveries?.[idx];
|
|
||||||
if (delivery) {
|
|
||||||
const productQty = delivery.products.reduce(
|
|
||||||
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
|
||||||
0
|
|
||||||
);
|
);
|
||||||
if (productQty > 0 && value > 0) {
|
} else {
|
||||||
const perItem = value / productQty;
|
setSelectedDeliveries([]);
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.delivery_cost_per_item`,
|
|
||||||
perItem
|
|
||||||
);
|
|
||||||
} else if (value === 0) {
|
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[formik.values.deliveries, setSelectedDeliveries]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleDeliveryCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const idx = Number(e.target.name.replace('delivery-', ''));
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedDeliveries((prev) => [...prev, idx]);
|
||||||
|
} else {
|
||||||
|
setSelectedDeliveries((prev) => prev.filter((i) => i !== idx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedDeliveries]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryProductChange = useCallback(
|
||||||
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.products.0.product`, val);
|
||||||
|
formik.setFieldTouched(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${deliveryIdx}.products.0.product_id`,
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliverySupplierChange = useCallback(
|
||||||
|
(deliveryIdx: number, val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier`, true);
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.supplier`, val);
|
||||||
|
formik.setFieldTouched(`deliveries.${deliveryIdx}.supplier_id`, true);
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${deliveryIdx}.supplier_id`,
|
||||||
|
(val as OptionType)?.value
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryDocumentChange = useCallback(
|
||||||
|
(deliveryIdx: number, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error('Ukuran dokumen maksimal 5 MB!');
|
||||||
|
e.target.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formik.setFieldValue(`deliveries.${deliveryIdx}.document`, file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeliveryCostChange = useCallback((idx: number, value: number) => {
|
||||||
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost`, value);
|
||||||
|
|
||||||
|
const delivery = formik.values.deliveries?.[idx];
|
||||||
|
if (delivery) {
|
||||||
|
const productQty = delivery.products.reduce(
|
||||||
|
(sum, p) => sum + (parseInt(p.product_qty.toString()) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
if (productQty > 0 && value > 0) {
|
||||||
|
const perItem = value / productQty;
|
||||||
|
formik.setFieldValue(
|
||||||
|
`deliveries.${idx}.delivery_cost_per_item`,
|
||||||
|
perItem
|
||||||
|
);
|
||||||
|
} else if (value === 0) {
|
||||||
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDeliveryCostPerItemChange = useCallback(
|
const handleDeliveryCostPerItemChange = useCallback(
|
||||||
(idx: number, value: number) => {
|
(idx: number, value: number) => {
|
||||||
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
formik.setFieldValue(`deliveries.${idx}.delivery_cost_per_item`, value);
|
||||||
@@ -492,7 +677,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[formik]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeliveryCostChangeWrapper = useCallback(
|
const handleDeliveryCostChangeWrapper = useCallback(
|
||||||
@@ -967,45 +1152,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang asal...'
|
placeholder='Pilih gudang asal...'
|
||||||
value={formik.values.source_warehouse}
|
value={formik.values.source_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleSourceWarehouseChange}
|
||||||
const newSourceWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newSourceWarehouseId) {
|
|
||||||
if (
|
|
||||||
newSourceWarehouseId ===
|
|
||||||
formik.values.destination_warehouse_id
|
|
||||||
) {
|
|
||||||
const destinationWarehouseName =
|
|
||||||
(
|
|
||||||
formik.values
|
|
||||||
.destination_warehouse as WarehouseOptionType
|
|
||||||
)?.label || 'gudang tujuan';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang asal tidak boleh sama dengan ${destinationWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('source_warehouse', true);
|
|
||||||
formik.setFieldValue('source_warehouse', val);
|
|
||||||
formik.setFieldTouched('source_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'source_warehouse_id',
|
|
||||||
newSourceWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
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 &&
|
||||||
@@ -1066,44 +1216,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
label='Gudang'
|
label='Gudang'
|
||||||
placeholder='Pilih gudang tujuan...'
|
placeholder='Pilih gudang tujuan...'
|
||||||
value={formik.values.destination_warehouse}
|
value={formik.values.destination_warehouse}
|
||||||
onChange={(val) => {
|
onChange={handleDestinationWarehouseChange}
|
||||||
const newDestinationWarehouseId = (val as WarehouseOptionType)
|
|
||||||
?.value;
|
|
||||||
|
|
||||||
if (newDestinationWarehouseId) {
|
|
||||||
if (
|
|
||||||
newDestinationWarehouseId ===
|
|
||||||
formik.values.source_warehouse_id
|
|
||||||
) {
|
|
||||||
const sourceWarehouseName =
|
|
||||||
(formik.values.source_warehouse as WarehouseOptionType)
|
|
||||||
?.label || 'gudang asal';
|
|
||||||
|
|
||||||
toast.error(
|
|
||||||
`Tidak bisa memilih gudang yang sama. Gudang tujuan tidak boleh sama dengan ${sourceWarehouseName}.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formik.setFieldTouched('destination_warehouse', true);
|
|
||||||
formik.setFieldValue('destination_warehouse', val);
|
|
||||||
formik.setFieldTouched('destination_warehouse_id', true);
|
|
||||||
formik.setFieldValue(
|
|
||||||
'destination_warehouse_id',
|
|
||||||
newDestinationWarehouseId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
formik.errors.destination_warehouse_id ===
|
|
||||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
|
||||||
) {
|
|
||||||
formik.setFieldError('destination_warehouse_id', undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
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)
|
||||||
@@ -1173,18 +1290,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedProducts.length &&
|
selectedProducts.length &&
|
||||||
formik.values.products?.length > 0
|
formik.values.products?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleProductSelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts(
|
|
||||||
formik.values.products?.map((_, idx) => idx) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1221,17 +1327,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`product-${idx}`}
|
name={`product-${idx}`}
|
||||||
checked={selectedProducts.includes(idx)}
|
checked={selectedProducts.includes(idx)}
|
||||||
onChange={(
|
onChange={handleProductCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedProducts([...selectedProducts, idx]);
|
|
||||||
} else {
|
|
||||||
setSelectedProducts(
|
|
||||||
selectedProducts.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1243,26 +1339,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
required
|
||||||
value={product.product ?? undefined}
|
value={product.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => handleProductChange(idx, val)}
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`products.${idx}.product_id`,
|
|
||||||
(val as ProductWarehouseOptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={productWarehouseOptions}
|
options={productWarehouseOptions}
|
||||||
onInputChange={setProductWarehouseSelectInputValue}
|
onInputChange={setProductWarehouseSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProductWarehouses}
|
||||||
isLoading={isLoadingProductWarehouses}
|
isLoading={isLoadingProductWarehouses}
|
||||||
isDisabled={
|
isDisabled={
|
||||||
type === 'detail' ||
|
type === 'detail' ||
|
||||||
@@ -1386,19 +1466,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
selectedDeliveries.length &&
|
selectedDeliveries.length &&
|
||||||
formik.values.deliveries?.length > 0
|
formik.values.deliveries?.length > 0
|
||||||
}
|
}
|
||||||
onChange={(
|
onChange={handleDeliverySelectAllChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
formik.values.deliveries?.map(
|
|
||||||
(_, idx) => idx
|
|
||||||
) ?? []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1481,20 +1549,7 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name={`delivery-${idx}`}
|
name={`delivery-${idx}`}
|
||||||
checked={selectedDeliveries.includes(idx)}
|
checked={selectedDeliveries.includes(idx)}
|
||||||
onChange={(
|
onChange={handleDeliveryCheckboxChange}
|
||||||
e: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedDeliveries([
|
|
||||||
...selectedDeliveries,
|
|
||||||
idx,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setSelectedDeliveries(
|
|
||||||
selectedDeliveries.filter((i) => i !== idx)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
wrapper: 'flex justify-center',
|
wrapper: 'flex justify-center',
|
||||||
checkbox: 'checkbox checkbox-sm',
|
checkbox: 'checkbox checkbox-sm',
|
||||||
@@ -1507,24 +1562,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih produk...'
|
placeholder='Pilih produk...'
|
||||||
value={delivery.products[0]?.product ?? undefined}
|
value={delivery.products[0]?.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliveryProductChange(idx, val)
|
||||||
`deliveries.${idx}.products.0.product`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.products.0.product_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={getFilteredProductWarehouseOptions()}
|
options={getFilteredProductWarehouseOptions()}
|
||||||
isDisabled={type === 'detail'}
|
isDisabled={type === 'detail'}
|
||||||
isClearable
|
isClearable
|
||||||
@@ -1575,24 +1615,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
required
|
required
|
||||||
placeholder='Pilih supplier...'
|
placeholder='Pilih supplier...'
|
||||||
value={delivery.supplier}
|
value={delivery.supplier}
|
||||||
onChange={(val) => {
|
onChange={(val) =>
|
||||||
formik.setFieldTouched(
|
handleDeliverySupplierChange(idx, val)
|
||||||
`deliveries.${idx}.supplier`,
|
}
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier`,
|
|
||||||
val
|
|
||||||
);
|
|
||||||
formik.setFieldTouched(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.supplier_id`,
|
|
||||||
(val as OptionType)?.value
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
options={supplierOptions}
|
options={supplierOptions}
|
||||||
onInputChange={setSupplierSelectInputValue}
|
onInputChange={setSupplierSelectInputValue}
|
||||||
isLoading={isLoadingSuppliers}
|
isLoading={isLoadingSuppliers}
|
||||||
@@ -1684,20 +1709,9 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
|||||||
<FileInput
|
<FileInput
|
||||||
accept='.pdf,.jpg,.jpeg,.png'
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
name={`deliveries.${idx}.document`}
|
name={`deliveries.${idx}.document`}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const file = e.target.files?.[0];
|
handleDeliveryDocumentChange(idx, e)
|
||||||
if (file) {
|
}
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
toast.error('Ukuran dokumen maksimal 5 MB!');
|
|
||||||
e.target.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formik.setFieldValue(
|
|
||||||
`deliveries.${idx}.document`,
|
|
||||||
file
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...isRepeaterInputError(
|
{...isRepeaterInputError(
|
||||||
'deliveries',
|
'deliveries',
|
||||||
'document',
|
'document',
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const InventoryProductDetail = ({
|
|||||||
<td>:</td>
|
<td>:</td>
|
||||||
<td>
|
<td>
|
||||||
{inventoryProduct?.tax
|
{inventoryProduct?.tax
|
||||||
? formatCurrency(inventoryProduct?.tax)
|
? formatNumber(inventoryProduct?.tax) + '%'
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
|
|||||||
import { TableToolbar } from '@/components/table/TableToolbar';
|
import { TableToolbar } from '@/components/table/TableToolbar';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatCurrency, formatDate } from '@/lib/helper';
|
import { cn, formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
MarketingApi,
|
MarketingApi,
|
||||||
SalesOrderApi,
|
SalesOrderApi,
|
||||||
@@ -33,6 +33,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { useAuth } from '@/services/hooks/useAuth';
|
import { useAuth } from '@/services/hooks/useAuth';
|
||||||
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
import { CustomerApi, ProductApi } from '@/services/api/master-data';
|
||||||
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
import { MARKETING_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
const RowsOptionsMenu = ({
|
const RowsOptionsMenu = ({
|
||||||
type = 'dropdown',
|
type = 'dropdown',
|
||||||
@@ -184,12 +185,16 @@ const MarketingTable = () => {
|
|||||||
const {
|
const {
|
||||||
options: productsOptions,
|
options: productsOptions,
|
||||||
isLoadingOptions: isLoadingProductsOptions,
|
isLoadingOptions: isLoadingProductsOptions,
|
||||||
|
setInputValue: setProductsInputValue,
|
||||||
|
loadMore: loadMoreProducts,
|
||||||
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
options: customersOptions,
|
options: customersOptions,
|
||||||
isLoadingOptions: isLoadingCustomersOptions,
|
isLoadingOptions: isLoadingCustomersOptions,
|
||||||
|
setInputValue: setCustomersInputValue,
|
||||||
|
loadMore: loadMoreCustomers,
|
||||||
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
} = useSelect(CustomerApi.basePath, 'id', 'name', '', {
|
||||||
limit: 'limit',
|
limit: 'limit',
|
||||||
});
|
});
|
||||||
@@ -400,6 +405,8 @@ const MarketingTable = () => {
|
|||||||
.join(',') || ''
|
.join(',') || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onInputChange={setProductsInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreProducts}
|
||||||
isMulti
|
isMulti
|
||||||
/>
|
/>
|
||||||
{/* select status */}
|
{/* select status */}
|
||||||
@@ -444,6 +451,8 @@ const MarketingTable = () => {
|
|||||||
(value as OptionType)?.value.toString() || ''
|
(value as OptionType)?.value.toString() || ''
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onInputChange={setCustomersInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomers}
|
||||||
/>
|
/>
|
||||||
</TableRowSizeSelector>
|
</TableRowSizeSelector>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,8 +521,53 @@ const MarketingTable = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'latest_approval.step_name',
|
accessorKey: 'approval.step_name',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
|
cell: (props) => {
|
||||||
|
const approval = props.row.original.latest_approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-lg px-2 w-full flex flex-row justify-start whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
color={
|
||||||
|
isRejected
|
||||||
|
? 'error'
|
||||||
|
: isApproved
|
||||||
|
? approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:circle'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
color={
|
||||||
|
approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'customer.name',
|
accessorKey: 'customer.name',
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
formatCurrency,
|
formatCurrency,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
|
formatTitleCase,
|
||||||
formatVechicleNumber,
|
formatVechicleNumber,
|
||||||
} from '@/lib/helper';
|
} from '@/lib/helper';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +35,7 @@ import toast from 'react-hot-toast';
|
|||||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
|
import Badge from '@/components/Badge';
|
||||||
|
|
||||||
const MarketingDetail = ({
|
const MarketingDetail = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -121,6 +123,10 @@ const MarketingDetail = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const approval = initialValues?.latest_approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex flex-col w-full gap-4'>
|
<div className='flex flex-col w-full gap-4'>
|
||||||
@@ -230,7 +236,46 @@ const MarketingDetail = ({
|
|||||||
<tr>
|
<tr>
|
||||||
<td className='font-semibold'>Status</td>
|
<td className='font-semibold'>Status</td>
|
||||||
<td>:</td>
|
<td>:</td>
|
||||||
<td>{initialValues?.latest_approval?.step_name}</td>
|
<td>
|
||||||
|
<Badge
|
||||||
|
variant='soft'
|
||||||
|
className={{
|
||||||
|
badge:
|
||||||
|
'rounded-lg px-2 w-fit flex flex-row justify-start whitespace-nowrap',
|
||||||
|
}}
|
||||||
|
color={
|
||||||
|
isRejected
|
||||||
|
? 'error'
|
||||||
|
: isApproved
|
||||||
|
? approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='mdi:circle'
|
||||||
|
width={12}
|
||||||
|
height={12}
|
||||||
|
color={
|
||||||
|
approval?.step_number == 1
|
||||||
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='font-semibold'>Tanggal Penjualan</td>
|
<td className='font-semibold'>Tanggal Penjualan</td>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
type MarketingSchemaType = {
|
type MarketingSchemaType = {
|
||||||
customer_id: number | undefined;
|
customer_id: number | undefined;
|
||||||
sales_person_id: number | undefined;
|
sales_person_id: number | undefined;
|
||||||
|
sales_person:
|
||||||
|
| {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
| null;
|
||||||
customer:
|
customer:
|
||||||
| {
|
| {
|
||||||
value: number;
|
value: number;
|
||||||
@@ -33,7 +40,11 @@ type DeliveryOrderSchemaType = {
|
|||||||
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
customer_id: Yup.number().required('Customer wajib diisi!'),
|
customer_id: Yup.number().required('Customer wajib diisi!'),
|
||||||
sales_person_id: Yup.number().required('Sales Person wajib diisi!'),
|
sales_person_id: Yup.number().required('Sales wajib diisi!'),
|
||||||
|
sales_person: Yup.object({
|
||||||
|
value: Yup.number().required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
customer: Yup.object({
|
customer: Yup.object({
|
||||||
value: Yup.number().required(),
|
value: Yup.number().required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/for
|
|||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
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 { CreatedUser } from '@/types/api/api-general';
|
||||||
|
import { UserApi } from '@/services/api/user';
|
||||||
|
|
||||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||||
@@ -244,7 +246,15 @@ const MarketingForm = ({
|
|||||||
const {
|
const {
|
||||||
options: customerOptions,
|
options: customerOptions,
|
||||||
isLoadingOptions: isLoadingCustomerOptions,
|
isLoadingOptions: isLoadingCustomerOptions,
|
||||||
|
setInputValue: setInputCustomerValue,
|
||||||
|
loadMore: loadMoreCustomer,
|
||||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||||
|
const {
|
||||||
|
options: salesOptions,
|
||||||
|
isLoadingOptions: isLoadingSalesOptions,
|
||||||
|
setInputValue: setInputSalesValue,
|
||||||
|
loadMore: loadMoreSales,
|
||||||
|
} = useSelect<CreatedUser>(UserApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// ================== SETUP FORMIK ==================
|
// ================== SETUP FORMIK ==================
|
||||||
const formikInitialValues = useMemo<
|
const formikInitialValues = useMemo<
|
||||||
@@ -255,6 +265,12 @@ const MarketingForm = ({
|
|||||||
notes: initialValues?.notes || undefined,
|
notes: initialValues?.notes || undefined,
|
||||||
customer_id: initialValues?.customer?.id || undefined,
|
customer_id: initialValues?.customer?.id || undefined,
|
||||||
sales_person_id: initialValues?.sales_person?.id || 1,
|
sales_person_id: initialValues?.sales_person?.id || 1,
|
||||||
|
sales_person: initialValues?.sales_person
|
||||||
|
? {
|
||||||
|
value: initialValues.sales_person.id,
|
||||||
|
label: initialValues.sales_person.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
customer: initialValues?.customer
|
customer: initialValues?.customer
|
||||||
? {
|
? {
|
||||||
value: initialValues.customer.id,
|
value: initialValues.customer.id,
|
||||||
@@ -345,6 +361,8 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const memoSalesOrder = formik.values.sales_order;
|
||||||
|
|
||||||
// ================== FORM REPEATER HANDLER ==================
|
// ================== FORM REPEATER HANDLER ==================
|
||||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -443,18 +461,37 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const handleChangeSalesPerson = useCallback(
|
||||||
|
(val: OptionType | OptionType[] | null) => {
|
||||||
|
formik.setFieldValue('sales_person_id', (val as OptionType)?.value);
|
||||||
|
formik.setFieldValue('sales_person', val as OptionType);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
deleteModal.openModal();
|
deleteModal.openModal();
|
||||||
}, [deleteModal]);
|
}, [deleteModal]);
|
||||||
|
|
||||||
// ================== SALES ORDER HANDLER ==================
|
// ================== SALES ORDER HANDLER ==================
|
||||||
const handleDeleteSO = useCallback((id: number) => {
|
const handleDeleteSO = useCallback(
|
||||||
const currentProducts = formik.values.sales_order;
|
(id: number) => {
|
||||||
formik.setFieldValue(
|
const currentProducts = formik.values.sales_order;
|
||||||
'sales_order',
|
formik.setFieldValue(
|
||||||
currentProducts.filter((p) => p.id != id)
|
'sales_order',
|
||||||
);
|
currentProducts.filter((p) => p.id != id)
|
||||||
}, []);
|
);
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
|
const handleEditSO = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
const currentProducts = formik.values.sales_order;
|
||||||
|
const selectedProduct = currentProducts.find((p) => p.id == id);
|
||||||
|
setSelectedMarketingProduct(selectedProduct ?? null);
|
||||||
|
addSOModal.openModal();
|
||||||
|
},
|
||||||
|
[memoSalesOrder]
|
||||||
|
);
|
||||||
const handleBulkDeleteSO = useCallback(() => {
|
const handleBulkDeleteSO = useCallback(() => {
|
||||||
const currentProducts = formik.values.sales_order;
|
const currentProducts = formik.values.sales_order;
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -464,7 +501,7 @@ const MarketingForm = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
setRowSOSelection({});
|
setRowSOSelection({});
|
||||||
}, [selectedRowSOIds]);
|
}, [selectedRowSOIds, memoSalesOrder]);
|
||||||
const handleAddSOClick = useCallback(() => {
|
const handleAddSOClick = useCallback(() => {
|
||||||
setSelectedMarketingProduct(null);
|
setSelectedMarketingProduct(null);
|
||||||
addSOModal.openModal();
|
addSOModal.openModal();
|
||||||
@@ -500,7 +537,7 @@ const MarketingForm = ({
|
|||||||
|
|
||||||
addSOModal.closeModal();
|
addSOModal.closeModal();
|
||||||
},
|
},
|
||||||
[addSOModal]
|
[addSOModal, memoSalesOrder]
|
||||||
);
|
);
|
||||||
|
|
||||||
// ================== DELIVERY ORDER HANDLER ==================
|
// ================== DELIVERY ORDER HANDLER ==================
|
||||||
@@ -545,8 +582,30 @@ const MarketingForm = ({
|
|||||||
},
|
},
|
||||||
[addDOModal]
|
[addDOModal]
|
||||||
);
|
);
|
||||||
|
const handleDeleteDO = useCallback(
|
||||||
const memoSalesOrder = formik.values.sales_order;
|
async (id: number) => {
|
||||||
|
setDeliveryOrderValues((prev) =>
|
||||||
|
prev.map((product) =>
|
||||||
|
product.id === id
|
||||||
|
? {
|
||||||
|
...product,
|
||||||
|
...{
|
||||||
|
unit_price: '',
|
||||||
|
total_weight: '',
|
||||||
|
qty: '',
|
||||||
|
avg_weight: '',
|
||||||
|
total_price: '',
|
||||||
|
delivery_date: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: product
|
||||||
|
)
|
||||||
|
);
|
||||||
|
addDOModal.closeModal();
|
||||||
|
setSelectedDeliveryProduct(null);
|
||||||
|
},
|
||||||
|
[addDOModal]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||||
@@ -580,6 +639,7 @@ const MarketingForm = ({
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'bg-white w-full',
|
wrapper: 'bg-white w-full',
|
||||||
}}
|
}}
|
||||||
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
|
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -588,6 +648,8 @@ const MarketingForm = ({
|
|||||||
isLoading={isLoadingCustomerOptions}
|
isLoading={isLoadingCustomerOptions}
|
||||||
value={formik.values.customer}
|
value={formik.values.customer}
|
||||||
onChange={handleChangeCustomer}
|
onChange={handleChangeCustomer}
|
||||||
|
onInputChange={setInputCustomerValue}
|
||||||
|
onMenuScrollToBottom={loadMoreCustomer}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
formik.touched.customer_id && Boolean(formik.errors.customer_id)
|
||||||
}
|
}
|
||||||
@@ -595,7 +657,9 @@ const MarketingForm = ({
|
|||||||
isClearable
|
isClearable
|
||||||
placeholder='Pilih Pelanggan'
|
placeholder='Pilih Pelanggan'
|
||||||
isDisabled={
|
isDisabled={
|
||||||
formType === 'add_deliver' || formType === 'edit_deliver'
|
formType === 'add_deliver' ||
|
||||||
|
formType === 'edit_deliver' ||
|
||||||
|
formType === 'edit'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DateInput
|
<DateInput
|
||||||
@@ -617,6 +681,7 @@ const MarketingForm = ({
|
|||||||
className={{
|
className={{
|
||||||
wrapper: 'bg-white w-full',
|
wrapper: 'bg-white w-full',
|
||||||
}}
|
}}
|
||||||
|
variant='bordered'
|
||||||
>
|
>
|
||||||
<MemoizedSalesOrderProductTable
|
<MemoizedSalesOrderProductTable
|
||||||
formType={formType}
|
formType={formType}
|
||||||
@@ -625,6 +690,7 @@ const MarketingForm = ({
|
|||||||
setRowSelection={setRowSOSelection}
|
setRowSelection={setRowSOSelection}
|
||||||
selectedRowIds={selectedRowSOIds}
|
selectedRowIds={selectedRowSOIds}
|
||||||
onDelete={handleDeleteSO}
|
onDelete={handleDeleteSO}
|
||||||
|
onEdit={handleEditSO}
|
||||||
onBulkDelete={handleBulkDeleteSO}
|
onBulkDelete={handleBulkDeleteSO}
|
||||||
onAddProductClick={handleAddSOClick}
|
onAddProductClick={handleAddSOClick}
|
||||||
/>
|
/>
|
||||||
@@ -644,6 +710,7 @@ const MarketingForm = ({
|
|||||||
formType={formType}
|
formType={formType}
|
||||||
data={deliveryOrderValues}
|
data={deliveryOrderValues}
|
||||||
onEdit={handleEditDO}
|
onEdit={handleEditDO}
|
||||||
|
onDelete={handleDeleteDO}
|
||||||
onAddProductClick={handleAddDOClick}
|
onAddProductClick={handleAddDOClick}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -651,19 +718,42 @@ const MarketingForm = ({
|
|||||||
|
|
||||||
{/* Input Notes */}
|
{/* Input Notes */}
|
||||||
<div className='grid sm:grid-cols-2 gap-3'>
|
<div className='grid sm:grid-cols-2 gap-3'>
|
||||||
<DebouncedTextArea
|
<div className='flex flex-col h-full items-end gap-3'>
|
||||||
required
|
<SelectInput
|
||||||
name='notes'
|
label='Sales'
|
||||||
label='Catatan'
|
options={salesOptions}
|
||||||
rows={3}
|
isLoading={isLoadingSalesOptions}
|
||||||
placeholder='Masukan catatan penjualan'
|
value={formik.values.sales_person}
|
||||||
value={formik.values.notes}
|
onChange={handleChangeSalesPerson}
|
||||||
onChange={formik.handleChange}
|
onInputChange={setInputSalesValue}
|
||||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
onMenuScrollToBottom={loadMoreSales}
|
||||||
errorMessage={formik.errors.notes}
|
isError={
|
||||||
disabled={formType === 'add_deliver' || formType === 'edit_deliver'}
|
formik.touched.sales_person_id &&
|
||||||
/>
|
Boolean(formik.errors.sales_person_id)
|
||||||
<div className='flex flex-col h-full justify-between items-end py-6'>
|
}
|
||||||
|
errorMessage={formik.errors.sales_person_id}
|
||||||
|
isClearable
|
||||||
|
placeholder='Pilih Sales'
|
||||||
|
isDisabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DebouncedTextArea
|
||||||
|
required
|
||||||
|
name='notes'
|
||||||
|
label='Catatan'
|
||||||
|
rows={3}
|
||||||
|
placeholder='Masukan catatan penjualan'
|
||||||
|
value={formik.values.notes}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||||
|
errorMessage={formik.errors.notes}
|
||||||
|
disabled={
|
||||||
|
formType === 'add_deliver' || formType === 'edit_deliver'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex flex-col h-full justify-end items-end'>
|
||||||
<span>Total Penjualan</span>
|
<span>Total Penjualan</span>
|
||||||
<span className='text-lg font-semibold'>
|
<span className='text-lg font-semibold'>
|
||||||
{formatCurrency(grandTotal)}{' '}
|
{formatCurrency(grandTotal)}{' '}
|
||||||
|
|||||||
+101
-36
@@ -18,6 +18,11 @@ import * as Yup from 'yup';
|
|||||||
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 useSWR from 'swr';
|
||||||
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
|
|
||||||
|
const roundWeight = (value: number) => Number(value.toFixed(2));
|
||||||
|
const roundPrice = (value: number) => Math.round(value);
|
||||||
|
|
||||||
const DeliveryOrderProductForm = ({
|
const DeliveryOrderProductForm = ({
|
||||||
formState,
|
formState,
|
||||||
@@ -43,6 +48,17 @@ const DeliveryOrderProductForm = ({
|
|||||||
);
|
);
|
||||||
const [currentInput, setCurrentInput] = useState<string>('');
|
const [currentInput, setCurrentInput] = useState<string>('');
|
||||||
|
|
||||||
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.basePath + '/' + selectedProduct?.value
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProduct?.value
|
||||||
|
? ProductApi.getSingle(Number(selectedProduct?.value))
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
const salesOrder = salesOrders.find(
|
const salesOrder = salesOrders.find(
|
||||||
(item) => item.id === initialValues?.marketing_product_id
|
(item) => item.id === initialValues?.marketing_product_id
|
||||||
);
|
);
|
||||||
@@ -90,6 +106,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
await onUpdateForm?.(values.marketing_product_id as number, values);
|
await onUpdateForm?.(values.marketing_product_id as number, values);
|
||||||
}
|
}
|
||||||
handleResetForm();
|
handleResetForm();
|
||||||
|
setSelectedProduct(null);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,27 +125,65 @@ const DeliveryOrderProductForm = ({
|
|||||||
marketing_product: undefined,
|
marketing_product: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setSelectedProduct(null);
|
// setSelectedProduct(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
|
||||||
formik.values;
|
|
||||||
|
|
||||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
const qty = Number(formik.values.qty || 0);
|
||||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
const avgWeight = Number(formik.values.avg_weight || 0);
|
||||||
formik.setFieldValue('total_price', Number(qty) * Number(unit_price));
|
const totalWeight = Number(formik.values.total_weight || 0);
|
||||||
} else if (qty && total_price && field === 'total_price') {
|
const unitPrice = Number(formik.values.unit_price || 0);
|
||||||
formik.setFieldValue('unit_price', Number(total_price) / Number(qty));
|
const totalPrice = Number(formik.values.total_price || 0);
|
||||||
|
|
||||||
|
if (qty <= 0) return;
|
||||||
|
|
||||||
|
switch (field) {
|
||||||
|
// ===== SOURCE FIELDS =====
|
||||||
|
case 'qty': {
|
||||||
|
if (avgWeight > 0) {
|
||||||
|
formik.setFieldValue('total_weight', roundWeight(qty * avgWeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
case 'avg_weight': {
|
||||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
if (avgWeight > 0) {
|
||||||
formik.setFieldValue('total_weight', Number(qty) * Number(avg_weight));
|
const tw = roundWeight(qty * avgWeight);
|
||||||
} else if (qty && total_weight && field === 'total_weight') {
|
formik.setFieldValue('total_weight', tw);
|
||||||
formik.setFieldValue('avg_weight', Number(total_weight) / Number(qty));
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'unit_price': {
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TOTAL EDITABLE =====
|
||||||
|
case 'total_weight': {
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'total_price': {
|
||||||
|
if (totalPrice > 0) {
|
||||||
|
formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -183,7 +238,7 @@ const DeliveryOrderProductForm = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
<div className='grid sm:grid-cols-3 gap-4'>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={options}
|
options={options}
|
||||||
label='Produk'
|
label='Produk'
|
||||||
@@ -287,7 +342,9 @@ const DeliveryOrderProductForm = ({
|
|||||||
isError={Boolean(formik.errors.vehicle_number)}
|
isError={Boolean(formik.errors.vehicle_number)}
|
||||||
errorMessage={formik.errors.vehicle_number}
|
errorMessage={formik.errors.vehicle_number}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='divider my-6'></div>
|
||||||
|
<div className='grid sm:grid-cols-3 gap-4'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Kuantitas'
|
label='Kuantitas'
|
||||||
@@ -301,33 +358,28 @@ const DeliveryOrderProductForm = ({
|
|||||||
isError={Boolean(formik.errors.qty)}
|
isError={Boolean(formik.errors.qty)}
|
||||||
errorMessage={formik.errors.qty}
|
errorMessage={formik.errors.qty}
|
||||||
placeholder='Masukan Kuantitas'
|
placeholder='Masukan Kuantitas'
|
||||||
|
endAdornment={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm text-gray-500'>
|
||||||
|
{isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
formik.values.marketing_product_id
|
formik.values.marketing_product_id
|
||||||
? 'Stok dijual: ' +
|
? 'Stok dijual: ' +
|
||||||
salesOrders?.find(
|
salesOrders?.find(
|
||||||
(item) => item.id === formik.values.marketing_product_id
|
(item) => item.id === formik.values.marketing_product_id
|
||||||
)?.qty
|
)?.qty +
|
||||||
|
' ' +
|
||||||
|
(isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: '')
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className='divider my-6'></div>
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4'>
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label='Avg. Bobot (Kg)'
|
|
||||||
name='avg_weight'
|
|
||||||
value={formik.values.avg_weight}
|
|
||||||
onChange={(e) => {
|
|
||||||
formik.handleChange(e);
|
|
||||||
setCurrentInput(e.target.name);
|
|
||||||
}}
|
|
||||||
onBlur={() => handleBlurField('avg_weight')}
|
|
||||||
isError={Boolean(formik.errors.avg_weight)}
|
|
||||||
errorMessage={formik.errors.avg_weight}
|
|
||||||
placeholder='Masukan Bobot Rata-rata'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Satuan (Rp)'
|
label='Harga Satuan (Rp)'
|
||||||
@@ -342,7 +394,20 @@ const DeliveryOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.unit_price}
|
errorMessage={formik.errors.unit_price}
|
||||||
placeholder='Masukan Harga Satuan'
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label='Avg. Bobot (Kg)'
|
||||||
|
name='avg_weight'
|
||||||
|
value={formik.values.avg_weight}
|
||||||
|
onChange={(e) => {
|
||||||
|
formik.handleChange(e);
|
||||||
|
setCurrentInput(e.target.name);
|
||||||
|
}}
|
||||||
|
onBlur={() => handleBlurField('avg_weight')}
|
||||||
|
isError={Boolean(formik.errors.avg_weight)}
|
||||||
|
errorMessage={formik.errors.avg_weight}
|
||||||
|
placeholder='Masukan Bobot Rata-rata'
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Total Bobot (Kg)'
|
label='Total Bobot (Kg)'
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ type SalesOrderProductSchemaType = {
|
|||||||
avg_weight: string | number | undefined;
|
avg_weight: string | number | undefined;
|
||||||
total_price: string | number | undefined;
|
total_price: string | number | undefined;
|
||||||
vehicle_number?: string | undefined;
|
vehicle_number?: string | undefined;
|
||||||
|
uom?: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaType> =
|
||||||
@@ -57,6 +58,7 @@ export const SalesOrderProductSchema: Yup.ObjectSchema<SalesOrderProductSchemaTy
|
|||||||
total_price: Yup.number()
|
total_price: Yup.number()
|
||||||
.min(1, 'Total Penjualan wajib diisi!')
|
.min(1, 'Total Penjualan wajib diisi!')
|
||||||
.required('Total Penjualan wajib diisi!'),
|
.required('Total Penjualan wajib diisi!'),
|
||||||
|
uom: Yup.string().nullable().optional().notRequired(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SalesOrderProductFormValues = Yup.InferType<
|
export type SalesOrderProductFormValues = Yup.InferType<
|
||||||
|
|||||||
+133
-59
@@ -11,7 +11,7 @@ import SelectInput, {
|
|||||||
useSelect,
|
useSelect,
|
||||||
} from '@/components/input/SelectInput';
|
} from '@/components/input/SelectInput';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { ProductApi, UomApi, WarehouseApi } from '@/services/api/master-data';
|
||||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||||
import NumberInput from '@/components/input/NumberInput';
|
import NumberInput from '@/components/input/NumberInput';
|
||||||
@@ -26,6 +26,10 @@ import PatternInput from '@/components/input/PatternInput';
|
|||||||
import Alert from '@/components/Alert';
|
import Alert from '@/components/Alert';
|
||||||
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 useSWR from 'swr';
|
||||||
|
|
||||||
|
const roundWeight = (value: number) => Number(value.toFixed(2));
|
||||||
|
const roundPrice = (value: number) => Math.round(value);
|
||||||
|
|
||||||
const SalesOrderProductForm = ({
|
const SalesOrderProductForm = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
@@ -39,21 +43,35 @@ const SalesOrderProductForm = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||||
const [currentInput, setCurrentInput] = useState<string>('');
|
const [currentInput, setCurrentInput] = useState<string>('');
|
||||||
|
const [selectedProductWarehouse, setSelectedProductWarehouse] =
|
||||||
|
useState<ProductWarehouse | null>(null);
|
||||||
|
|
||||||
|
// ============ Fetch Data ============
|
||||||
|
const { data: productData } = useSWR(
|
||||||
|
selectedProductWarehouse?.product_id
|
||||||
|
? ProductApi.basePath + '/' + selectedProductWarehouse?.product_id
|
||||||
|
: null,
|
||||||
|
() =>
|
||||||
|
selectedProductWarehouse?.product_id
|
||||||
|
? ProductApi.getSingle(selectedProductWarehouse?.product_id)
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
// ============ Formik ============
|
// ============ Formik ============
|
||||||
const formik = useFormik<SalesOrderProductFormValues>({
|
const formik = useFormik<SalesOrderProductFormValues>({
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
vehicle_number: initialValues?.vehicle_number || '',
|
||||||
kandang_id: initialValues?.kandang_id || undefined,
|
kandang_id: initialValues?.kandang_id || undefined,
|
||||||
kandang: initialValues?.kandang || undefined,
|
kandang: initialValues?.kandang || null,
|
||||||
product_warehouse: initialValues?.product_warehouse || undefined,
|
product_warehouse: initialValues?.product_warehouse || null,
|
||||||
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
product_warehouse_id: initialValues?.product_warehouse_id || undefined,
|
||||||
unit_price: initialValues?.unit_price || undefined,
|
unit_price: initialValues?.unit_price || '',
|
||||||
total_weight: initialValues?.total_weight || undefined,
|
total_weight: initialValues?.total_weight || '',
|
||||||
qty: initialValues?.qty || undefined,
|
qty: initialValues?.qty || '',
|
||||||
avg_weight: initialValues?.avg_weight || undefined,
|
avg_weight: initialValues?.avg_weight || '',
|
||||||
total_price: initialValues?.total_price || undefined,
|
total_price: initialValues?.total_price || '',
|
||||||
|
uom: initialValues?.uom || '',
|
||||||
},
|
},
|
||||||
validationSchema: SalesOrderProductSchema,
|
validationSchema: SalesOrderProductSchema,
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
@@ -69,17 +87,21 @@ const SalesOrderProductForm = ({
|
|||||||
const {
|
const {
|
||||||
options: kandangSourceOptions,
|
options: kandangSourceOptions,
|
||||||
isLoadingOptions: isLoadingKandangSourceOptions,
|
isLoadingOptions: isLoadingKandangSourceOptions,
|
||||||
|
setInputValue: setKandangInputValue,
|
||||||
|
loadMore: loadMoreKandang,
|
||||||
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: warehouseSourceOptions,
|
options: warehouseSourceOptions,
|
||||||
rawData: warehouseSourceRawData,
|
rawData: warehouseSourceRawData,
|
||||||
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
isLoadingOptions: isLoadingWarehouseSourceOptions,
|
||||||
|
setInputValue: setWarehouseInputValue,
|
||||||
|
loadMore: loadMoreWarehouse,
|
||||||
} = useSelect<ProductWarehouse>(
|
} = useSelect<ProductWarehouse>(
|
||||||
ProductWarehouseApi.basePath,
|
ProductWarehouseApi.basePath,
|
||||||
'id',
|
'id',
|
||||||
'product.name',
|
'product.name',
|
||||||
'search',
|
'',
|
||||||
{
|
{
|
||||||
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
warehouse_id: formik.values.kandang_id?.toString() ?? '',
|
||||||
}
|
}
|
||||||
@@ -112,6 +134,7 @@ const SalesOrderProductForm = ({
|
|||||||
const productWarehouse = warehouseSourceRawData?.data.find(
|
const productWarehouse = warehouseSourceRawData?.data.find(
|
||||||
(item: ProductWarehouse) => item.id === newId
|
(item: ProductWarehouse) => item.id === newId
|
||||||
);
|
);
|
||||||
|
setSelectedProductWarehouse(productWarehouse || null);
|
||||||
formik.setFieldValue('qty', productWarehouse?.quantity);
|
formik.setFieldValue('qty', productWarehouse?.quantity);
|
||||||
handleBlurField('qty');
|
handleBlurField('qty');
|
||||||
} else {
|
} else {
|
||||||
@@ -139,40 +162,78 @@ const SalesOrderProductForm = ({
|
|||||||
|
|
||||||
const handleBlurField = (field: string) => {
|
const handleBlurField = (field: string) => {
|
||||||
setCurrentInput(field);
|
setCurrentInput(field);
|
||||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
|
||||||
formik.values;
|
|
||||||
|
|
||||||
if (field === 'unit_price' || field === 'total_price' || field === 'qty') {
|
const qty = Number(formik.values.qty || 0);
|
||||||
if (qty && unit_price && (field === 'unit_price' || field === 'qty')) {
|
const avgWeight = Number(formik.values.avg_weight || 0);
|
||||||
formik.setFieldValue(
|
const totalWeight = Number(formik.values.total_weight || 0);
|
||||||
'total_price',
|
const unitPrice = Number(formik.values.unit_price || 0);
|
||||||
(qty as number) * (unit_price as number)
|
const totalPrice = Number(formik.values.total_price || 0);
|
||||||
);
|
|
||||||
} else if (qty && total_price && field === 'total_price') {
|
if (qty <= 0) return;
|
||||||
formik.setFieldValue(
|
|
||||||
'unit_price',
|
switch (field) {
|
||||||
(total_price as number) / (qty as number)
|
// ===== SOURCE FIELDS =====
|
||||||
);
|
case 'qty': {
|
||||||
|
if (avgWeight > 0) {
|
||||||
|
formik.setFieldValue('total_weight', roundWeight(qty * avgWeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (field === 'avg_weight' || field === 'total_weight' || field === 'qty') {
|
case 'avg_weight': {
|
||||||
if (qty && avg_weight && (field === 'avg_weight' || field === 'qty')) {
|
if (avgWeight > 0) {
|
||||||
formik.setFieldValue(
|
const tw = roundWeight(qty * avgWeight);
|
||||||
'total_weight',
|
formik.setFieldValue('total_weight', tw);
|
||||||
(qty as number) * (avg_weight as number)
|
|
||||||
);
|
if (unitPrice > 0) {
|
||||||
} else if (qty && total_weight && field === 'total_weight') {
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
formik.setFieldValue(
|
}
|
||||||
'avg_weight',
|
}
|
||||||
(total_weight as number) / (qty as number)
|
break;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
case 'unit_price': {
|
||||||
|
if (unitPrice > 0) {
|
||||||
|
formik.setFieldValue('total_price', roundPrice(qty * unitPrice));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== TOTAL EDITABLE =====
|
||||||
|
case 'total_weight': {
|
||||||
|
if (totalWeight > 0) {
|
||||||
|
formik.setFieldValue('avg_weight', roundWeight(totalWeight / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'total_price': {
|
||||||
|
if (totalPrice > 0) {
|
||||||
|
formik.setFieldValue('unit_price', roundPrice(totalPrice / qty));
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
|
formik,
|
||||||
|
{
|
||||||
|
onBeforeSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleBlurField(currentInput);
|
||||||
|
formik.setFieldValue(
|
||||||
|
'uom',
|
||||||
|
isResponseSuccess(productData) ? productData?.data?.uom.name : ''
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -188,7 +249,7 @@ const SalesOrderProductForm = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
||||||
<PatternInput
|
<PatternInput
|
||||||
name='vehicle_number'
|
name='vehicle_number'
|
||||||
label='No. Polisi'
|
label='No. Polisi'
|
||||||
@@ -215,6 +276,8 @@ const SalesOrderProductForm = ({
|
|||||||
value={formik.values.kandang}
|
value={formik.values.kandang}
|
||||||
onChange={kandangChangeHandler}
|
onChange={kandangChangeHandler}
|
||||||
isClearable
|
isClearable
|
||||||
|
onInputChange={setKandangInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
formik.touched.kandang_id && Boolean(formik.errors.kandang_id)
|
||||||
}
|
}
|
||||||
@@ -228,6 +291,8 @@ const SalesOrderProductForm = ({
|
|||||||
isLoading={isLoadingWarehouseSourceOptions}
|
isLoading={isLoadingWarehouseSourceOptions}
|
||||||
value={formik.values.product_warehouse}
|
value={formik.values.product_warehouse}
|
||||||
onChange={warehouseChangeHandler}
|
onChange={warehouseChangeHandler}
|
||||||
|
onInputChange={setWarehouseInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouse}
|
||||||
isClearable
|
isClearable
|
||||||
placeholder={
|
placeholder={
|
||||||
formik.values.kandang_id
|
formik.values.kandang_id
|
||||||
@@ -243,6 +308,9 @@ const SalesOrderProductForm = ({
|
|||||||
}
|
}
|
||||||
errorMessage={formik.errors.product_warehouse_id}
|
errorMessage={formik.errors.product_warehouse_id}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='divider my-6'></div>
|
||||||
|
<div className='grid sm:grid-cols-3 gap-4 z-200'>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Kuantitas'
|
label='Kuantitas'
|
||||||
@@ -256,6 +324,15 @@ const SalesOrderProductForm = ({
|
|||||||
isError={formik.touched.qty && Boolean(formik.errors.qty)}
|
isError={formik.touched.qty && Boolean(formik.errors.qty)}
|
||||||
errorMessage={formik.errors.qty}
|
errorMessage={formik.errors.qty}
|
||||||
placeholder='Masukan Kuantitas'
|
placeholder='Masukan Kuantitas'
|
||||||
|
endAdornment={
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<span className='text-sm text-gray-500'>
|
||||||
|
{isResponseSuccess(productData)
|
||||||
|
? productData?.data?.uom.name
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
bottomLabel={
|
bottomLabel={
|
||||||
isResponseSuccess(warehouseSourceRawData) &&
|
isResponseSuccess(warehouseSourceRawData) &&
|
||||||
formik.values.product_warehouse_id
|
formik.values.product_warehouse_id
|
||||||
@@ -264,32 +341,13 @@ const SalesOrderProductForm = ({
|
|||||||
(item) => item.id === formik.values.product_warehouse_id
|
(item) => item.id === formik.values.product_warehouse_id
|
||||||
)?.quantity ?? 0
|
)?.quantity ?? 0
|
||||||
)} ${
|
)} ${
|
||||||
warehouseSourceRawData?.data?.find(
|
isResponseSuccess(productData)
|
||||||
(item) => item.id === formik.values.product_warehouse_id
|
? productData?.data?.uom.name
|
||||||
)?.product?.uom?.name ?? ''
|
: ''
|
||||||
}`
|
}`
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className='divider my-6'></div>
|
|
||||||
<div className='grid sm:grid-cols-2 gap-4 z-200'>
|
|
||||||
<NumberInput
|
|
||||||
required
|
|
||||||
label='Avg. Bobot (Kg)'
|
|
||||||
name='avg_weight'
|
|
||||||
value={formik.values.avg_weight}
|
|
||||||
onChange={(e) => {
|
|
||||||
formik.handleChange(e);
|
|
||||||
setCurrentInput(e.target.name);
|
|
||||||
}}
|
|
||||||
onBlur={() => handleBlurField('avg_weight')}
|
|
||||||
isError={
|
|
||||||
formik.touched.avg_weight && Boolean(formik.errors.avg_weight)
|
|
||||||
}
|
|
||||||
errorMessage={formik.errors.avg_weight}
|
|
||||||
placeholder='Masukan Bobot Rata-rata'
|
|
||||||
/>
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Harga Satuan (Rp)'
|
label='Harga Satuan (Rp)'
|
||||||
@@ -306,6 +364,22 @@ const SalesOrderProductForm = ({
|
|||||||
errorMessage={formik.errors.unit_price}
|
errorMessage={formik.errors.unit_price}
|
||||||
placeholder='Masukan Harga Satuan'
|
placeholder='Masukan Harga Satuan'
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
label='Avg. Bobot (Kg)'
|
||||||
|
name='avg_weight'
|
||||||
|
value={formik.values.avg_weight}
|
||||||
|
onChange={(e) => {
|
||||||
|
formik.handleChange(e);
|
||||||
|
setCurrentInput(e.target.name);
|
||||||
|
}}
|
||||||
|
onBlur={() => handleBlurField('avg_weight')}
|
||||||
|
isError={
|
||||||
|
formik.touched.avg_weight && Boolean(formik.errors.avg_weight)
|
||||||
|
}
|
||||||
|
errorMessage={formik.errors.avg_weight}
|
||||||
|
placeholder='Masukan Bobot Rata-rata'
|
||||||
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
required
|
||||||
label='Total Bobot (Kg)'
|
label='Total Bobot (Kg)'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type DeliveryOrderProductTableProps = {
|
|||||||
data: DeliveryOrderProductFormValues[];
|
data: DeliveryOrderProductFormValues[];
|
||||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||||
onEdit: (id: number) => void;
|
onEdit: (id: number) => void;
|
||||||
|
onDelete: (id: number) => void;
|
||||||
onAddProductClick: () => void;
|
onAddProductClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,10 +24,13 @@ const DeliveryOrderProductTable = ({
|
|||||||
data,
|
data,
|
||||||
formType,
|
formType,
|
||||||
onEdit,
|
onEdit,
|
||||||
|
onDelete,
|
||||||
onAddProductClick,
|
onAddProductClick,
|
||||||
}: DeliveryOrderProductTableProps) => {
|
}: DeliveryOrderProductTableProps) => {
|
||||||
const onEditRef = useRef(onEdit);
|
const onEditRef = useRef(onEdit);
|
||||||
onEditRef.current = onEdit;
|
onEditRef.current = onEdit;
|
||||||
|
const onDeleteRef = useRef(onDelete);
|
||||||
|
onDeleteRef.current = onDelete;
|
||||||
|
|
||||||
const canAddData = data.filter((item) => !Boolean(item.qty));
|
const canAddData = data.filter((item) => !Boolean(item.qty));
|
||||||
|
|
||||||
@@ -144,16 +148,29 @@ const DeliveryOrderProductTable = ({
|
|||||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||||
<>
|
<>
|
||||||
{props.row.original.qty && (
|
{props.row.original.qty && (
|
||||||
<Button
|
<>
|
||||||
color='warning'
|
<Button
|
||||||
className='px-2 py-1 text-sm'
|
color='warning'
|
||||||
onClick={() =>
|
className='px-2 py-1 text-sm'
|
||||||
onEditRef.current(props.row.original.id as number)
|
onClick={() =>
|
||||||
}
|
onEditRef.current(props.row.original.id as number)
|
||||||
type='button'
|
}
|
||||||
>
|
type='button'
|
||||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
>
|
||||||
</Button>
|
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color='error'
|
||||||
|
className='px-2 py-1 text-sm'
|
||||||
|
onClick={() =>
|
||||||
|
onDeleteRef.current(props.row.original.id as number)
|
||||||
|
}
|
||||||
|
type='button'
|
||||||
|
disabled={!!props.row.original.do_number}
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:delete' width={16} height={16} /> Hapus
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!props.row.original.qty && '-'}
|
{!props.row.original.qty && '-'}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type SalesOrderProductTableProps = {
|
|||||||
>;
|
>;
|
||||||
selectedRowIds: number[];
|
selectedRowIds: number[];
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
|
onEdit: (id: number) => void;
|
||||||
onBulkDelete: () => void;
|
onBulkDelete: () => void;
|
||||||
onAddProductClick: () => void;
|
onAddProductClick: () => void;
|
||||||
};
|
};
|
||||||
@@ -34,11 +35,14 @@ const SalesOrderProductTable = ({
|
|||||||
setRowSelection,
|
setRowSelection,
|
||||||
selectedRowIds,
|
selectedRowIds,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onEdit,
|
||||||
onBulkDelete,
|
onBulkDelete,
|
||||||
onAddProductClick,
|
onAddProductClick,
|
||||||
}: SalesOrderProductTableProps) => {
|
}: SalesOrderProductTableProps) => {
|
||||||
const onDeleteRef = useRef(onDelete);
|
const onDeleteRef = useRef(onDelete);
|
||||||
onDeleteRef.current = onDelete;
|
onDeleteRef.current = onDelete;
|
||||||
|
const onEditRef = useRef(onEdit);
|
||||||
|
onEditRef.current = onEdit;
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -92,17 +96,26 @@ const SalesOrderProductTable = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.total_weight as string)),
|
formatNumber(parseFloat(row.total_weight as string), undefined, 0, 5),
|
||||||
header: 'Total Bobot (Kg)',
|
header: 'Total Bobot (Kg)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.qty as string)),
|
formatNumber(parseFloat(row.qty as string)),
|
||||||
header: 'Kuantitas',
|
header: 'Kuantitas',
|
||||||
|
cell: ({ row }: { row: TanStack.Row<SalesOrderProductFormValues> }) =>
|
||||||
|
formatNumber(
|
||||||
|
parseFloat(row.original.qty as string),
|
||||||
|
undefined,
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
) +
|
||||||
|
' ' +
|
||||||
|
(row.original.uom ?? ''),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row: SalesOrderProductFormValues) =>
|
accessorFn: (row: SalesOrderProductFormValues) =>
|
||||||
formatNumber(parseFloat(row.avg_weight as string)),
|
formatNumber(parseFloat(row.avg_weight as string), undefined, 0, 5),
|
||||||
header: 'Avg. Bobot (Kg)',
|
header: 'Avg. Bobot (Kg)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,6 +129,14 @@ const SalesOrderProductTable = ({
|
|||||||
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
props: TanStack.CellContext<SalesOrderProductFormValues, unknown>
|
||||||
) => (
|
) => (
|
||||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||||
|
<Button
|
||||||
|
color='warning'
|
||||||
|
className='p-1'
|
||||||
|
onClick={() => onEditRef.current(props.row.original.id as number)}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
<Icon icon='mdi:pencil' width={16} height={16} /> Edit
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color='error'
|
color='error'
|
||||||
className='p-1'
|
className='p-1'
|
||||||
@@ -124,7 +145,7 @@ const SalesOrderProductTable = ({
|
|||||||
}
|
}
|
||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:trash' width={16} height={16} />
|
<Icon icon='mdi:trash' width={16} height={16} /> Hapus
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatDate, formatNumber, formatVechicleNumber } from '@/lib/helper';
|
|||||||
import { format } from 'path';
|
import { format } from 'path';
|
||||||
import { date } from 'yup';
|
import { date } from 'yup';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface DeliveryOrderExportProps {
|
interface DeliveryOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -23,7 +24,7 @@ const DeliveryOrderExport = ({
|
|||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
alert('No sales order data available');
|
toast.error('No sales order data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsGeneratingPDF(true);
|
setIsGeneratingPDF(true);
|
||||||
@@ -40,8 +41,7 @@ const DeliveryOrderExport = ({
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error);
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
alert('Failed to generate PDF. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Document, Image, Page, pdf, Text, View } from '@react-pdf/renderer';
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { formatDate, formatNumber } from '@/lib/helper';
|
import { formatDate, formatNumber } from '@/lib/helper';
|
||||||
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
import pdfStyles from '@/components/pages/marketing/pdf/styles/MarketingPDFStyles';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
interface SalesOrderExportProps {
|
interface SalesOrderExportProps {
|
||||||
data?: Marketing;
|
data?: Marketing;
|
||||||
@@ -17,7 +18,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
|
|
||||||
const handleDownloadPDF = async () => {
|
const handleDownloadPDF = async () => {
|
||||||
if (!salesData) {
|
if (!salesData) {
|
||||||
alert('No sales order data available');
|
toast.error('No sales order data available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsGeneratingPDF(true);
|
setIsGeneratingPDF(true);
|
||||||
@@ -32,8 +33,7 @@ const SalesOrderExport = ({ data }: SalesOrderExportProps) => {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error);
|
toast.error('Failed to generate PDF. Please try again.');
|
||||||
alert('Failed to generate PDF. Please try again.');
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsGeneratingPDF(false);
|
setIsGeneratingPDF(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Area } from '@/types/api/master-data/area';
|
import { Area } from '@/types/api/master-data/area';
|
||||||
import { AreaApi } from '@/services/api/master-data';
|
import { AreaApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const AreasTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await AreaApi.delete(selectedArea?.id as number);
|
const deleteResponse = await AreaApi.delete(selectedArea?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshAreas();
|
refreshAreas();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Bank } from '@/types/api/master-data/bank';
|
import { Bank } from '@/types/api/master-data/bank';
|
||||||
import { BankApi } from '@/services/api/master-data';
|
import { BankApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -177,7 +177,14 @@ const BanksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await BankApi.delete(selectedBank?.id as number);
|
const deleteResponse = await BankApi.delete(selectedBank?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshBanks();
|
refreshBanks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { CustomerApi } from '@/services/api/master-data';
|
import { CustomerApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -186,7 +186,16 @@ const CustomersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await CustomerApi.delete(selectedCustomer?.id as number);
|
const deleteResponse = await CustomerApi.delete(
|
||||||
|
selectedCustomer?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshCustomers();
|
refreshCustomers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Fcr } from '@/types/api/master-data/fcr';
|
import { Fcr } from '@/types/api/master-data/fcr';
|
||||||
import { FcrApi } from '@/services/api/master-data';
|
import { FcrApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const FcrsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FcrApi.delete(selectedFcr?.id as number);
|
const deleteResponse = await FcrApi.delete(selectedFcr?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFcrs();
|
refreshFcrs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
|||||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||||
|
|
||||||
const RowsOptions = ({
|
const RowsOptions = ({
|
||||||
@@ -33,22 +33,6 @@ const RowsOptions = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RowOptionsMenuWrapper type={type}>
|
<RowOptionsMenuWrapper type={type}>
|
||||||
<RequirePermission permissions='lti.master.flocks.update'>
|
|
||||||
<Button
|
|
||||||
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
|
||||||
variant='ghost'
|
|
||||||
color='warning'
|
|
||||||
className='justify-start text-sm'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:edit-outline'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
className='justify-start text-sm'
|
|
||||||
/>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
</RequirePermission>
|
|
||||||
<RequirePermission permissions='lti.master.flocks.detail'>
|
<RequirePermission permissions='lti.master.flocks.detail'>
|
||||||
<Button
|
<Button
|
||||||
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
|
||||||
@@ -65,6 +49,22 @@ const RowsOptions = ({
|
|||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
|
<RequirePermission permissions='lti.master.flocks.update'>
|
||||||
|
<Button
|
||||||
|
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
|
||||||
|
variant='ghost'
|
||||||
|
color='warning'
|
||||||
|
className='justify-start text-sm'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:edit-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className='justify-start text-sm'
|
||||||
|
/>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</RequirePermission>
|
||||||
<RequirePermission permissions='lti.master.flocks.delete'>
|
<RequirePermission permissions='lti.master.flocks.delete'>
|
||||||
<Button
|
<Button
|
||||||
onClick={deleteClickHandler}
|
onClick={deleteClickHandler}
|
||||||
@@ -182,7 +182,14 @@ const FlockTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FlockApi.delete(selectedFlock?.id as number);
|
const deleteResponse = await FlockApi.delete(selectedFlock?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshFlocks();
|
refreshFlocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
|||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
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 { toast } from 'react-hot-toast';
|
||||||
|
import Alert from '@/components/Alert';
|
||||||
|
|
||||||
interface FlockCustomProps {
|
interface FlockCustomProps {
|
||||||
formType?: 'add' | 'edit' | 'detail';
|
formType?: 'add' | 'edit' | 'detail';
|
||||||
@@ -37,7 +39,13 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await FlockApi.delete(initialValues?.id as number);
|
const deleteFlockRes = await FlockApi.delete(initialValues?.id as number);
|
||||||
|
if (deleteFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(deleteFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(deleteFlockRes?.message as string);
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
@@ -68,12 +76,29 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
|
|
||||||
// cek type form yang disubmit
|
// cek type form yang disubmit
|
||||||
switch (formType) {
|
switch (formType) {
|
||||||
case 'add':
|
case 'add': {
|
||||||
await FlockApi.create(payload);
|
const createFlockRes = await FlockApi.create(payload);
|
||||||
|
if (createFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(createFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(createFlockRes?.message as string);
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
}
|
||||||
await FlockApi.update(initialValues?.id as number, payload);
|
case 'edit': {
|
||||||
|
const updateFlockRes = await FlockApi.update(
|
||||||
|
initialValues?.id as number,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
if (updateFlockRes?.status === 'error') {
|
||||||
|
setFlockFormErrorMessage(updateFlockRes.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(updateFlockRes?.message as string);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -174,6 +199,24 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
<AlertErrorList formErrorList={formErrorList} onClose={close} />
|
||||||
|
{flockFormErrorMessage && (
|
||||||
|
<Alert color='error' className='w-full'>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:error-outline'
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
{flockFormErrorMessage}
|
||||||
|
<Button
|
||||||
|
onClick={() => setFlockFormErrorMessage('')}
|
||||||
|
variant='link'
|
||||||
|
className='ml-auto p-0 w-fit text-white'
|
||||||
|
color='none'
|
||||||
|
>
|
||||||
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||||
|
</Button>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{formType !== 'detail' && (
|
{formType !== 'detail' && (
|
||||||
<div
|
<div
|
||||||
@@ -197,17 +240,6 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{flockFormErrorMessage && (
|
|
||||||
<div role='alert' className='alert alert-error'>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:error-outline'
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
/>
|
|
||||||
<span>{flockFormErrorMessage}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { KandangApi } from '@/services/api/master-data';
|
import { KandangApi } from '@/services/api/master-data';
|
||||||
import { cn, formatNumber } from '@/lib/helper';
|
import { cn, formatNumber } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -199,7 +199,16 @@ const KandangsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await KandangApi.delete(selectedKandang?.id as number);
|
const deleteResponse = await KandangApi.delete(
|
||||||
|
selectedKandang?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshKandangs();
|
refreshKandangs();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Location } from '@/types/api/master-data/location';
|
import { Location } from '@/types/api/master-data/location';
|
||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -186,7 +186,16 @@ const LocationsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await LocationApi.delete(selectedLocation?.id as number);
|
const deleteResponse = await LocationApi.delete(
|
||||||
|
selectedLocation?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshLocations();
|
refreshLocations();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Nonstock } from '@/types/api/master-data/nonstock';
|
import { Nonstock } from '@/types/api/master-data/nonstock';
|
||||||
import { NonstockApi } from '@/services/api/master-data';
|
import { NonstockApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -198,7 +198,16 @@ const NonstocksTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await NonstockApi.delete(selectedNonstock?.id as number);
|
const deleteResponse = await NonstockApi.delete(
|
||||||
|
selectedNonstock?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshNonstocks();
|
refreshNonstocks();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -78,7 +83,7 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
|
|||||||
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
const formikInitialValues = useMemo<NonstockFormValues>(() => {
|
||||||
return {
|
return {
|
||||||
name: initialValues?.name ?? '',
|
name: initialValues?.name ?? '',
|
||||||
uomId: initialValues?.uom_id ?? 0,
|
uomId: initialValues?.uom?.id ?? 0,
|
||||||
uom: initialValues?.uom
|
uom: initialValues?.uom
|
||||||
? {
|
? {
|
||||||
value: initialValues?.uom?.id,
|
value: initialValues?.uom?.id,
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { ProductCategory } from '@/types/api/master-data/product-category';
|
import { ProductCategory } from '@/types/api/master-data/product-category';
|
||||||
import { ProductCategoryApi } from '@/services/api/master-data';
|
import { ProductCategoryApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -170,7 +170,16 @@ const ProductCategoryTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductCategoryApi.delete(selectedProductCategory?.id as number);
|
const deleteResponse = await ProductCategoryApi.delete(
|
||||||
|
selectedProductCategory?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductCategories();
|
refreshProductCategories();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Product } from '@/types/api/master-data/product';
|
import { Product } from '@/types/api/master-data/product';
|
||||||
import { ProductApi } from '@/services/api/master-data';
|
import { ProductApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -230,8 +230,19 @@ const ProductsTable = () => {
|
|||||||
|
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
await ProductApi.delete(selectedProduct?.id as number);
|
|
||||||
|
const deleteResponse = await ProductApi.delete(
|
||||||
|
selectedProduct?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProducts();
|
refreshProducts();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
toast.success('Successfully delete Product!');
|
toast.success('Successfully delete Product!');
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ProductionStandard } from '@/types/api/master-data/production-standard'
|
|||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
@@ -94,9 +94,16 @@ const ProductionStandardTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await ProductionStandardApi.delete(
|
const deleteResponse = await ProductionStandardApi.delete(
|
||||||
selectedProductionStandard?.id as number
|
selectedProductionStandard?.id as number
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshProductionStandards();
|
refreshProductionStandards();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
+15
-19
@@ -2,34 +2,30 @@ import * as Yup from 'yup';
|
|||||||
|
|
||||||
// Schema for LAYING category (production_standard_details is required)
|
// Schema for LAYING category (production_standard_details is required)
|
||||||
const LayingRepeaterFormSchema = Yup.object({
|
const LayingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().required(
|
target_hen_day_production: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per hari wajib diisi!'
|
target_hen_house_production: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
target_egg_weight: Yup.number().required('Wajib diisi!'),
|
||||||
target_hen_house_production: Yup.number().required(
|
target_egg_mass: Yup.number().required('Wajib diisi!'),
|
||||||
'Produksi telur per kandang wajib diisi!'
|
standard_fcr: Yup.number().required('Wajib diisi!'),
|
||||||
),
|
|
||||||
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
|
|
||||||
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
|
|
||||||
standard_fcr: Yup.number().required('FCR wajib diisi!'),
|
|
||||||
}).required(),
|
}).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Schema for GROWING category (production_standard_details is optional)
|
// Schema for GROWING category (production_standard_details is optional)
|
||||||
const GrowingRepeaterFormSchema = Yup.object({
|
const GrowingRepeaterFormSchema = Yup.object({
|
||||||
week: Yup.number().required('Minggu wajib diisi!'),
|
week: Yup.number().required('Wajib diisi!'),
|
||||||
production_standard_uniformity_details: Yup.object({
|
production_standard_uniformity_details: Yup.object({
|
||||||
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
|
target_mean_bw: Yup.number().required('Wajib diisi!'),
|
||||||
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
|
max_depletion: Yup.number().required('Wajib diisi!'),
|
||||||
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
|
min_uniformity: Yup.number().required('Wajib diisi!'),
|
||||||
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
|
feed_intake: Yup.number().required('Wajib diisi!'),
|
||||||
}),
|
}),
|
||||||
production_standard_details: Yup.object({
|
production_standard_details: Yup.object({
|
||||||
target_hen_day_production: Yup.number().optional(),
|
target_hen_day_production: Yup.number().optional(),
|
||||||
|
|||||||
+86
-45
@@ -344,7 +344,7 @@ const ProductionStandardForm = ({
|
|||||||
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
const columns = useMemo<ColumnDef<TableRowsType>[]>(() => {
|
||||||
const baseColumns: ColumnDef<TableRowsType>[] = [
|
const baseColumns: ColumnDef<TableRowsType>[] = [
|
||||||
{
|
{
|
||||||
header: 'Minggu',
|
header: 'Week',
|
||||||
accessorKey: 'week',
|
accessorKey: 'week',
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
@@ -358,30 +358,40 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Hen Day',
|
header: 'Hen Day',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_day_production,
|
row.production_standard_details?.target_hen_day_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_day_production}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Hen House',
|
header: 'Hen House',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_hen_house_production,
|
row.production_standard_details?.target_hen_house_production,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_hen_house_production} pc`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Weight',
|
header: 'Egg Weight',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_weight,
|
row.production_standard_details?.target_egg_weight,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_weight} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Egg Mass',
|
header: 'Egg Mass',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.target_egg_mass,
|
row.production_standard_details?.target_egg_mass,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.target_egg_mass} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'FCR',
|
header: 'FCR',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_details?.standard_fcr,
|
row.production_standard_details?.standard_fcr,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_details?.standard_fcr} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -393,24 +403,32 @@ const ProductionStandardForm = ({
|
|||||||
header: 'Mean BW',
|
header: 'Mean BW',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.target_mean_bw,
|
row.production_standard_uniformity_details?.target_mean_bw,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.target_mean_bw} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Max Depletion',
|
header: 'Max Depletion',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.max_depletion,
|
row.production_standard_uniformity_details?.max_depletion,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.max_depletion}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Min Uniformity',
|
header: 'Min Uniformity',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.min_uniformity,
|
row.production_standard_uniformity_details?.min_uniformity,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.min_uniformity}%`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Feed Intake',
|
header: 'Feed Intake',
|
||||||
accessorFn: (row) =>
|
accessorFn: (row) =>
|
||||||
row.production_standard_uniformity_details?.feed_intake,
|
row.production_standard_uniformity_details?.feed_intake,
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.production_standard_uniformity_details?.feed_intake} g`,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -728,7 +746,52 @@ const ProductionStandardForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ===== Formik Error List =====
|
// ===== Formik Error List =====
|
||||||
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(formik);
|
const { formErrorList, close, handleFormSubmit } = useFormikErrorList(
|
||||||
|
formik,
|
||||||
|
{
|
||||||
|
onBeforeSubmit: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// For GROWING category, clear production_standard_details errors and set default values
|
||||||
|
if (formik.values.project_category === 'GROWING') {
|
||||||
|
// Set default values for production_standard_details
|
||||||
|
formik.values.details?.forEach((detail) => {
|
||||||
|
detail.production_standard_details = {
|
||||||
|
target_hen_day_production: 0,
|
||||||
|
target_hen_house_production: 0,
|
||||||
|
target_egg_weight: 0,
|
||||||
|
target_egg_mass: 0,
|
||||||
|
standard_fcr: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear any errors related to production_standard_details
|
||||||
|
const currentErrors = { ...formik.errors };
|
||||||
|
if (currentErrors.details && Array.isArray(currentErrors.details)) {
|
||||||
|
const cleanedDetails = currentErrors.details
|
||||||
|
.map((detailError) => {
|
||||||
|
if (detailError && typeof detailError === 'object') {
|
||||||
|
const { production_standard_details, ...rest } = detailError;
|
||||||
|
return Object.keys(rest).length > 0 ? rest : undefined;
|
||||||
|
}
|
||||||
|
return detailError;
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
(error): error is Exclude<typeof error, undefined> =>
|
||||||
|
error !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
currentErrors.details = (
|
||||||
|
cleanedDetails.length > 0 ? cleanedDetails : undefined
|
||||||
|
) as typeof currentErrors.details;
|
||||||
|
}
|
||||||
|
formik.setErrors(currentErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -821,19 +884,20 @@ const ProductionStandardForm = ({
|
|||||||
key={`row-${row.index}`}
|
key={`row-${row.index}`}
|
||||||
className='sticky bottom-0 bg-base-100 shadow-lg'
|
className='sticky bottom-0 bg-base-100 shadow-lg'
|
||||||
>
|
>
|
||||||
<td colSpan={colSpan} className='p-6'>
|
<td colSpan={colSpan} className='p-2'>
|
||||||
<form
|
<form
|
||||||
className='h-full w-full flex flex-col justify-end'
|
className='h-full w-full flex flex-col justify-end'
|
||||||
onSubmit={repeaterFormik.handleSubmit}
|
onSubmit={repeaterFormik.handleSubmit}
|
||||||
onReset={repeaterFormik.handleReset}
|
onReset={repeaterFormik.handleReset}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className='grid gap-2 items-start w-full'
|
||||||
'grid gap-4 items-start',
|
style={{
|
||||||
formik.values.project_category === 'LAYING'
|
gridTemplateColumns:
|
||||||
? 'grid-cols-10'
|
formik.values.project_category === 'LAYING'
|
||||||
: 'grid-cols-5'
|
? 'repeat(10, minmax(auto, 1fr)) minmax(auto, auto)'
|
||||||
)}
|
: 'repeat(4, minmax(auto, 1fr)) minmax(auto, auto)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
name='week'
|
name='week'
|
||||||
@@ -862,7 +926,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -894,11 +958,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Butir (pc)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
Butir
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -930,11 +990,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -959,17 +1015,13 @@ const ProductionStandardForm = ({
|
|||||||
name='production_standard_details.target_egg_mass'
|
name='production_standard_details.target_egg_mass'
|
||||||
label='Egg Mass'
|
label='Egg Mass'
|
||||||
placeholder='1'
|
placeholder='1'
|
||||||
|
bottomLabel='Gram (g)'
|
||||||
value={
|
value={
|
||||||
repeaterFormik.values
|
repeaterFormik.values
|
||||||
.production_standard_details?.target_egg_mass
|
.production_standard_details?.target_egg_mass
|
||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1000,11 +1052,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={getProductionDetailsError(
|
errorMessage={getProductionDetailsError(
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_details,
|
.production_standard_details,
|
||||||
@@ -1038,11 +1086,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
|
||||||
gr
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1072,7 +1116,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1102,7 +1146,7 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={<Icon icon='mdi:percent' />}
|
bottomLabel='Persen (%)'
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1132,11 +1176,8 @@ const ProductionStandardForm = ({
|
|||||||
}
|
}
|
||||||
onChange={repeaterFormik.handleChange}
|
onChange={repeaterFormik.handleChange}
|
||||||
onBlur={repeaterFormik.handleBlur}
|
onBlur={repeaterFormik.handleBlur}
|
||||||
endAdornment={
|
bottomLabel='Gram/Ekor (g)'
|
||||||
<div className='w-full h-full flex items-center justify-center'>
|
endAdornment
|
||||||
gr/ekor
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
errorMessage={
|
errorMessage={
|
||||||
repeaterFormik.errors
|
repeaterFormik.errors
|
||||||
.production_standard_uniformity_details
|
.production_standard_uniformity_details
|
||||||
@@ -1162,7 +1203,7 @@ const ProductionStandardForm = ({
|
|||||||
type='button'
|
type='button'
|
||||||
color='error'
|
color='error'
|
||||||
variant='outline'
|
variant='outline'
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
onClick={handleCancelEdit}
|
onClick={handleCancelEdit}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:close' /> Batal
|
<Icon icon='mdi:close' /> Batal
|
||||||
@@ -1178,7 +1219,7 @@ const ProductionStandardForm = ({
|
|||||||
<Button
|
<Button
|
||||||
type='submit'
|
type='submit'
|
||||||
color={editMode ? 'warning' : 'success'}
|
color={editMode ? 'warning' : 'success'}
|
||||||
className='min-w-24'
|
className='min-w-xs'
|
||||||
disabled={
|
disabled={
|
||||||
isAddingRow ||
|
isAddingRow ||
|
||||||
formik.values.project_category === ''
|
formik.values.project_category === ''
|
||||||
@@ -1195,7 +1236,7 @@ const ProductionStandardForm = ({
|
|||||||
variant='outline'
|
variant='outline'
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={toggleTableHeight}
|
onClick={toggleTableHeight}
|
||||||
className='absolute bottom-6 right-6'
|
className='absolute bottom-2 right-2'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon={
|
icon={
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
|||||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { SupplierApi } from '@/services/api/master-data';
|
import { SupplierApi } from '@/services/api/master-data';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -205,7 +205,16 @@ const SuppliersTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await SupplierApi.delete(selectedSupplier?.id as number);
|
const deleteResponse = await SupplierApi.delete(
|
||||||
|
selectedSupplier?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshSuppliers();
|
refreshSuppliers();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Uom } from '@/types/api/master-data/uom';
|
import { Uom } from '@/types/api/master-data/uom';
|
||||||
import { UomApi } from '@/services/api/master-data';
|
import { UomApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -164,7 +164,14 @@ const UomsTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await UomApi.delete(selectedUom?.id as number);
|
const deleteResponse = await UomApi.delete(selectedUom?.id as number);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshUoms();
|
refreshUoms();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import RequirePermission from '@/components/helper/RequirePermission';
|
|||||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||||
import { WarehouseApi } from '@/services/api/master-data';
|
import { WarehouseApi } from '@/services/api/master-data';
|
||||||
import { cn } from '@/lib/helper';
|
import { cn } from '@/lib/helper';
|
||||||
import { isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
|
|
||||||
@@ -220,7 +220,16 @@ const WarehousesTable = () => {
|
|||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
|
||||||
await WarehouseApi.delete(selectedWarehouse?.id as number);
|
const deleteResponse = await WarehouseApi.delete(
|
||||||
|
selectedWarehouse?.id as number
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isResponseError(deleteResponse)) {
|
||||||
|
toast.error(deleteResponse.message);
|
||||||
|
setIsDeleteLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
refreshWarehouses();
|
refreshWarehouses();
|
||||||
|
|
||||||
deleteModal.closeModal();
|
deleteModal.closeModal();
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,12 +75,12 @@ const ChickinFormKandang = ({
|
|||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color='success'
|
color='primary'
|
||||||
className={{
|
className={{
|
||||||
badge: 'rounded-lg px-2',
|
badge: 'rounded-lg px-2',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon icon='mdi:circle' width={12} height={12} color='success' />{' '}
|
<Icon icon='mdi:circle' width={12} height={12} color='primary' />{' '}
|
||||||
Aktif
|
Aktif
|
||||||
</Badge>
|
</Badge>
|
||||||
<div className='divider divider-horizontal p-0 m-0'></div>
|
<div className='divider divider-horizontal p-0 m-0'></div>
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ import Button from '@/components/Button';
|
|||||||
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
import FloatingActionsButton from '@/components/FloatingActionsButton';
|
||||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||||
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 ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||||
import Table from '@/components/Table';
|
import Table from '@/components/Table';
|
||||||
import { ROWS_OPTIONS } from '@/config/constant';
|
import { ROWS_OPTIONS } from '@/config/constant';
|
||||||
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate, formatTitleCase } from '@/lib/helper';
|
||||||
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
import { AreaApi, KandangApi, LocationApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
import { ProjectFlockApi } from '@/services/api/production/project-flock';
|
||||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||||
@@ -59,9 +62,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
const selectedRowIds = Object.keys(rowSelection)
|
const selectedRowIds = Object.keys(rowSelection)
|
||||||
.filter((id) => rowSelection[id])
|
.filter((id) => rowSelection[id])
|
||||||
.map((id) => parseInt(id));
|
.map((id) => parseInt(id));
|
||||||
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
|
|
||||||
const [areaSelectInputValue, setAreaSelectInputValue] = useState('');
|
|
||||||
const [kandangSelectInputValue, setKandangSelectInputValue] = useState('');
|
|
||||||
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
const [selectedArea, setSelectedArea] = useState<OptionType | null>(null);
|
||||||
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
const [selectedLocation, setSelectedLocation] = useState<OptionType | null>(
|
||||||
null
|
null
|
||||||
@@ -90,55 +90,25 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
{ revalidateOnMount: true }
|
{ revalidateOnMount: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const areaUrl = `${AreaApi.basePath}?${new URLSearchParams({
|
// ===== Fetch Data Select =====
|
||||||
search: areaSelectInputValue,
|
const {
|
||||||
limit: '100',
|
options: optionsArea,
|
||||||
}).toString()}`;
|
isLoadingOptions: isLoadingArea,
|
||||||
const { data: areas, isLoading: isLoadingAreas } = useSWR(
|
setInputValue: setAreaSelectInputValue,
|
||||||
areaUrl,
|
loadMore: loadMoreArea,
|
||||||
AreaApi.getAllFetcher
|
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||||
);
|
const {
|
||||||
|
options: optionsLocation,
|
||||||
const locationUrl = `${LocationApi.basePath}?${new URLSearchParams({
|
isLoadingOptions: isLoadingLocation,
|
||||||
search: locationSelectInputValue,
|
setInputValue: setLocationSelectInputValue,
|
||||||
area_id: selectedArea != null ? selectedArea.value.toString() : '',
|
loadMore: loadMoreLocation,
|
||||||
limit: '100',
|
} = useSelect(LocationApi.basePath, 'id', 'name');
|
||||||
}).toString()}`;
|
const {
|
||||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
options: optionsKandang,
|
||||||
locationUrl,
|
isLoadingOptions: isLoadingKandang,
|
||||||
LocationApi.getAllFetcher
|
setInputValue: setKandangSelectInputValue,
|
||||||
);
|
loadMore: loadMoreKandang,
|
||||||
|
} = useSelect(KandangApi.basePath, 'id', 'name');
|
||||||
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
|
||||||
search: kandangSelectInputValue,
|
|
||||||
location_id:
|
|
||||||
selectedLocation != null ? selectedLocation.value.toString() : '',
|
|
||||||
limit: '100',
|
|
||||||
}).toString()}`;
|
|
||||||
const { data: kandangs, isLoading: isLoadingKandang } = useSWR(
|
|
||||||
kandangUrl,
|
|
||||||
KandangApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
// ===== Data to Options Mapping ======
|
|
||||||
const optionsArea = isResponseSuccess(areas)
|
|
||||||
? areas?.data.map((area) => ({
|
|
||||||
value: area.id,
|
|
||||||
label: area.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsKandang = isResponseSuccess(kandangs)
|
|
||||||
? kandangs?.data.map((kandang) => ({
|
|
||||||
value: kandang.id,
|
|
||||||
label: kandang.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
const optionsLocation = isResponseSuccess(locations)
|
|
||||||
? locations?.data.map((location) => ({
|
|
||||||
value: location.id,
|
|
||||||
label: location.name,
|
|
||||||
}))
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// ====== HANDLER ======
|
// ====== HANDLER ======
|
||||||
const confirmationModalDeleteClickHandler = async () => {
|
const confirmationModalDeleteClickHandler = async () => {
|
||||||
@@ -284,7 +254,8 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
header: 'Status',
|
header: 'Status',
|
||||||
cell: (props) => {
|
cell: (props) => {
|
||||||
const approval = props.row.original.approval;
|
const approval = props.row.original.approval;
|
||||||
|
const isRejected = approval?.action == 'REJECTED';
|
||||||
|
const isApproved = approval?.action == 'APPROVED';
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
@@ -292,11 +263,17 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
|
badge: 'rounded-lg px-2 w-full flex flex-row justify-start',
|
||||||
}}
|
}}
|
||||||
color={
|
color={
|
||||||
approval?.step_number == 1
|
isRejected
|
||||||
? 'neutral'
|
? 'error'
|
||||||
: approval?.step_number == 2
|
: isApproved
|
||||||
? 'success'
|
? approval?.step_number == 1
|
||||||
: 'error'
|
? 'neutral'
|
||||||
|
: approval?.step_number == 2
|
||||||
|
? 'primary'
|
||||||
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
|
: 'neutral'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -307,11 +284,15 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
approval?.step_number == 1
|
approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: approval?.step_number == 2
|
: approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: 'error'
|
: approval?.step_number == 3
|
||||||
|
? 'success'
|
||||||
|
: 'neutral'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{approval?.step_name}
|
{isRejected
|
||||||
|
? 'Ditolak'
|
||||||
|
: formatTitleCase(approval?.step_name || '')}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -385,7 +366,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
<SelectInput
|
<SelectInput
|
||||||
label='Area'
|
label='Area'
|
||||||
options={optionsArea}
|
options={optionsArea}
|
||||||
isLoading={isLoadingAreas}
|
isLoading={isLoadingArea}
|
||||||
value={selectedArea}
|
value={selectedArea}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setSelectedArea(val as OptionType);
|
setSelectedArea(val as OptionType);
|
||||||
@@ -395,12 +376,13 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setAreaSelectInputValue}
|
onInputChange={setAreaSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreArea}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
label='Lokasi'
|
label='Lokasi'
|
||||||
options={optionsLocation}
|
options={optionsLocation}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocation}
|
||||||
value={selectedLocation}
|
value={selectedLocation}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
setSelectedLocation(val as OptionType);
|
setSelectedLocation(val as OptionType);
|
||||||
@@ -410,6 +392,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
@@ -425,6 +408,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onInputChange={setKandangSelectInputValue}
|
onInputChange={setKandangSelectInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreKandang}
|
||||||
isClearable
|
isClearable
|
||||||
/>
|
/>
|
||||||
<DebouncedTextInput
|
<DebouncedTextInput
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ const ProjectFlockDetail = ({
|
|||||||
projectFlock.approval?.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval?.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: projectFlock.approval?.step_number >= 3
|
: projectFlock.approval?.step_number == 3
|
||||||
? 'error'
|
? 'success'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={{
|
className={{
|
||||||
@@ -173,9 +173,9 @@ const ProjectFlockDetail = ({
|
|||||||
projectFlock.approval?.step_number == 1
|
projectFlock.approval?.step_number == 1
|
||||||
? 'neutral'
|
? 'neutral'
|
||||||
: projectFlock.approval?.step_number == 2
|
: projectFlock.approval?.step_number == 2
|
||||||
? 'success'
|
? 'primary'
|
||||||
: projectFlock.approval?.step_number >= 3
|
: projectFlock.approval?.step_number == 3
|
||||||
? 'error'
|
? 'success'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
@@ -273,7 +273,7 @@ const ProjectFlockDetail = ({
|
|||||||
<div className='flex flex-row gap-2'>
|
<div className='flex flex-row gap-2'>
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
color={'success'}
|
color={'primary'}
|
||||||
className={{
|
className={{
|
||||||
badge: 'rounded-lg px-2',
|
badge: 'rounded-lg px-2',
|
||||||
}}
|
}}
|
||||||
@@ -282,7 +282,7 @@ const ProjectFlockDetail = ({
|
|||||||
icon='mdi:circle'
|
icon='mdi:circle'
|
||||||
width={12}
|
width={12}
|
||||||
height={12}
|
height={12}
|
||||||
color={'success'}
|
color={'primary'}
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Kandang Aktif ({projectFlock.kandangs?.length})
|
Kandang Aktif ({projectFlock.kandangs?.length})
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -102,41 +102,54 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fetch Data
|
// Fetch Data
|
||||||
const { isLoadingOptions: isLoadingFlocks, options: optionsFlock } =
|
const {
|
||||||
useSelect(FlockApi.basePath, 'id', 'name');
|
setInputValue: setInputValueFlock,
|
||||||
|
isLoadingOptions: isLoadingFlocks,
|
||||||
|
options: optionsFlock,
|
||||||
|
loadMore: loadMoreFlock,
|
||||||
|
} = useSelect(FlockApi.basePath, 'id', 'name', '', {
|
||||||
|
project_category: selectedCategory,
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsArea, isLoadingOptions: isLoadingAreas } = useSelect(
|
const {
|
||||||
AreaApi.basePath,
|
setInputValue: setInputValueArea,
|
||||||
'id',
|
options: optionsArea,
|
||||||
'name'
|
isLoadingOptions: isLoadingAreas,
|
||||||
);
|
loadMore: loadMoreArea,
|
||||||
|
} = useSelect(AreaApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const { options: optionsLocation, isLoadingOptions: isLoadingLocations } =
|
const {
|
||||||
useSelect(LocationApi.basePath, 'id', 'name', '', {
|
options: optionsLocation,
|
||||||
area_id:
|
isLoadingOptions: isLoadingLocations,
|
||||||
selectedArea != ''
|
setInputValue: setInputValueLocation,
|
||||||
? selectedArea
|
loadMore: loadMoreLocation,
|
||||||
: ((initialValues?.area?.id ?? '') as string),
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
});
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
const { options: optionsFcr, isLoadingOptions: isLoadingFcrs } = useSelect(
|
const {
|
||||||
FcrApi.basePath,
|
options: optionsFcr,
|
||||||
'id',
|
isLoadingOptions: isLoadingFcrs,
|
||||||
'name'
|
setInputValue: setInputValueFcr,
|
||||||
);
|
loadMore: loadMoreFcr,
|
||||||
|
} = useSelect(FcrApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
options: optionsProductionStandards,
|
options: optionsProductionStandards,
|
||||||
isLoadingOptions: isLoadingProductionStandards,
|
isLoadingOptions: isLoadingProductionStandards,
|
||||||
|
setInputValue: setInputValueProductionStandard,
|
||||||
|
loadMore: loadMoreProductionStandard,
|
||||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||||
search: '',
|
|
||||||
project_category: selectedCategory,
|
project_category: selectedCategory,
|
||||||
});
|
});
|
||||||
|
|
||||||
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
const kandangUrl = `${KandangApi.basePath}?${new URLSearchParams({
|
||||||
search: '',
|
search: '',
|
||||||
location_id: selectedLocation == '' ? '0' : selectedLocation,
|
location_id: selectedLocation == '' ? '0' : selectedLocation,
|
||||||
limit: 'limit',
|
limit: '500',
|
||||||
}).toString()}`;
|
}).toString()}`;
|
||||||
const {
|
const {
|
||||||
data: kandang,
|
data: kandang,
|
||||||
@@ -153,6 +166,8 @@ const ProjectFlockForm = ({
|
|||||||
options: optionsNonstock,
|
options: optionsNonstock,
|
||||||
rawData: nonstocks,
|
rawData: nonstocks,
|
||||||
isLoadingOptions: isLoadingNonstocks,
|
isLoadingOptions: isLoadingNonstocks,
|
||||||
|
setInputValue: setInputValueNonstock,
|
||||||
|
loadMore: loadMoreNonstock,
|
||||||
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -542,15 +557,12 @@ const ProjectFlockForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
const onDeleteBudgetRowHandler = (nonstock_id: number, index?: number) => {
|
||||||
console.log(`nonstock_id: ${nonstock_id}, index: ${index}`);
|
|
||||||
if (!nonstock_id) {
|
if (!nonstock_id) {
|
||||||
const updatedBudgets = formik.values.project_budgets
|
const updatedBudgets = formik.values.project_budgets
|
||||||
.map((budget, i) => {
|
.map((budget, i) => {
|
||||||
if (i == index) {
|
if (i == index) {
|
||||||
console.log(`buget: ${null}, index: ${index}, i: ${i}`);
|
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
console.log(`buget: ${budget}, index: ${index}, i: ${i}`);
|
|
||||||
return budget;
|
return budget;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -722,6 +734,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.area_id && Boolean(formik.errors.area_id)
|
formik.touched.area_id && Boolean(formik.errors.area_id)
|
||||||
}
|
}
|
||||||
errorMessage={formik.errors.area_id as string}
|
errorMessage={formik.errors.area_id as string}
|
||||||
|
onInputChange={setInputValueArea}
|
||||||
|
onMenuScrollToBottom={loadMoreArea}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add'}
|
isDisabled={formType != 'add'}
|
||||||
/>
|
/>
|
||||||
@@ -740,6 +754,8 @@ const ProjectFlockForm = ({
|
|||||||
formik.touched.location_id &&
|
formik.touched.location_id &&
|
||||||
Boolean(formik.errors.location_id)
|
Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
|
onInputChange={setInputValueLocation}
|
||||||
|
onMenuScrollToBottom={loadMoreLocation}
|
||||||
errorMessage={formik.errors.location_id as string}
|
errorMessage={formik.errors.location_id as string}
|
||||||
isClearable
|
isClearable
|
||||||
isDisabled={formType != 'add' || disabledLocation}
|
isDisabled={formType != 'add' || disabledLocation}
|
||||||
@@ -766,6 +782,8 @@ const ProjectFlockForm = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
options={optionsFlock}
|
options={optionsFlock}
|
||||||
|
onInputChange={setInputValueFlock}
|
||||||
|
onMenuScrollToBottom={loadMoreFlock}
|
||||||
isLoading={isLoadingFlocks}
|
isLoading={isLoadingFlocks}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
formik.touched.flock_name && Boolean(formik.errors.flock_name)
|
||||||
@@ -781,6 +799,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'fcr');
|
optionChangeHandler(val, 'fcr');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueFcr}
|
||||||
|
onMenuScrollToBottom={loadMoreFcr}
|
||||||
options={optionsFcr}
|
options={optionsFcr}
|
||||||
isLoading={isLoadingFcrs}
|
isLoading={isLoadingFcrs}
|
||||||
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
isError={formik.touched.fcr_id && Boolean(formik.errors.fcr_id)}
|
||||||
@@ -808,6 +828,8 @@ const ProjectFlockForm = ({
|
|||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
optionChangeHandler(val, 'production_standard');
|
optionChangeHandler(val, 'production_standard');
|
||||||
}}
|
}}
|
||||||
|
onInputChange={setInputValueProductionStandard}
|
||||||
|
onMenuScrollToBottom={loadMoreProductionStandard}
|
||||||
options={optionsProductionStandards}
|
options={optionsProductionStandards}
|
||||||
isLoading={isLoadingProductionStandards}
|
isLoading={isLoadingProductionStandards}
|
||||||
isError={
|
isError={
|
||||||
@@ -892,6 +914,8 @@ const ProjectFlockForm = ({
|
|||||||
isLoading={isLoadingNonstocks}
|
isLoading={isLoadingNonstocks}
|
||||||
placeholder='Pilih barang non stock'
|
placeholder='Pilih barang non stock'
|
||||||
value={formik.values.project_budgets[index].nonstock}
|
value={formik.values.project_budgets[index].nonstock}
|
||||||
|
onInputChange={setInputValueNonstock}
|
||||||
|
onMenuScrollToBottom={loadMoreNonstock}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const updatedBudgets = [
|
const updatedBudgets = [
|
||||||
...formik.values.project_budgets,
|
...formik.values.project_budgets,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RefObject } from 'react';
|
|||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { SortingState, CellContext } from '@tanstack/react-table';
|
import { SortingState, CellContext } from '@tanstack/react-table';
|
||||||
import { cn, formatDate } from '@/lib/helper';
|
import { cn, formatDate, formatNumber } from '@/lib/helper';
|
||||||
import RequirePermission from '@/components/helper/RequirePermission';
|
import RequirePermission from '@/components/helper/RequirePermission';
|
||||||
import { useModal } from '@/components/Modal';
|
import { useModal } from '@/components/Modal';
|
||||||
import Modal from '@/components/Modal';
|
import Modal from '@/components/Modal';
|
||||||
@@ -656,34 +656,57 @@ const RecordingTable = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
const recording = row.original;
|
||||||
|
const isDisabled = isRecordingApproved(recording);
|
||||||
|
|
||||||
|
const handleToggleSelection = (e: unknown) => {
|
||||||
|
if (!isDisabled) {
|
||||||
|
row.getToggleSelectedHandler()(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={cn({ 'opacity-50': isDisabled })}>
|
||||||
<CheckboxInput
|
<CheckboxInput
|
||||||
name='row'
|
name='row'
|
||||||
checked={row.getIsSelected()}
|
checked={row.getIsSelected()}
|
||||||
indeterminate={row.getIsSomeSelected()}
|
indeterminate={row.getIsSomeSelected()}
|
||||||
onChange={row.getToggleSelectedHandler()}
|
onChange={handleToggleSelection}
|
||||||
|
disabled={isDisabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: '#',
|
header: 'No',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
tableFilterState.pageSize * (tableFilterState.page - 1) +
|
||||||
props.row.index +
|
props.row.index +
|
||||||
1,
|
1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nama Project',
|
header: 'Lokasi',
|
||||||
|
cell: (props) => props.row.original.location?.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Flock',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
`Project ${props.row.original.project_flock_kandang_id}`,
|
props.row.original.project_flock?.flock_name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Kandang',
|
||||||
|
cell: (props) => props.row.original.kandang?.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Periode',
|
||||||
|
cell: (props) => props.row.original.project_flock?.period || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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 (
|
||||||
@@ -695,18 +718,280 @@ const RecordingTable = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Umur (hari)',
|
header: 'Umur (hari)',
|
||||||
cell: (props) => props.row.original.day,
|
cell: (props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{props.row.original.day} (Minggu ke-
|
||||||
|
{props.row.original.project_flock.production_standart.week})
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'record_date',
|
|
||||||
header: 'Waktu Recording',
|
header: 'Waktu Recording',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
formatDate(props.row.original.record_datetime, 'DD MMMM YYYY'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Populasi Awal',
|
header: 'Populasi Akhir',
|
||||||
cell: (props) =>
|
cell: (props) =>
|
||||||
props.row.original.total_chick_qty?.toLocaleString() || '-',
|
props.row.original.project_flock?.total_chick_qty != null
|
||||||
|
? formatNumber(props.row.original.project_flock.total_chick_qty)
|
||||||
|
: '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fcr',
|
||||||
|
header: 'FCR',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'fcr_actual',
|
||||||
|
header: 'Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.fcr_value;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fcr_standard',
|
||||||
|
header: 'Standard',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.project_flock?.fcr?.fcr_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feed_intake',
|
||||||
|
header: 'Feed Intake (KG)',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'feed_intake_actual',
|
||||||
|
header: 'Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.feed_intake;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feed_intake_standard',
|
||||||
|
header: 'Standard',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.feed_intake_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mortality',
|
||||||
|
header: 'Mortality',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'cum_depletion_rate_actual',
|
||||||
|
header: 'Cum Depletion Rate',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.cum_depletion_rate;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'max_depletion_std',
|
||||||
|
header: 'Max Depletion Std',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.max_depletion_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'total_depletion',
|
||||||
|
header: 'Total Depletion',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.total_depletion_qty;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_production',
|
||||||
|
header: 'Egg Production',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'egg_mass_actual',
|
||||||
|
header: 'Egg Mass Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_mass;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_mass_standard',
|
||||||
|
header: 'Egg Mass Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.egg_mass_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_weight_actual',
|
||||||
|
header: 'Egg Weight Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.egg_weight;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'egg_weight_standard',
|
||||||
|
header: 'Egg Weight Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.egg_weight_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? formatNumber(value)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_performance',
|
||||||
|
header: 'Hen Performance',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
id: 'hen_day_actual',
|
||||||
|
header: 'Hen Day Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hen_day;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_day_standard',
|
||||||
|
header: 'Hen Day Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.hen_day_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_house_actual',
|
||||||
|
header: 'Hen House Actual',
|
||||||
|
cell: (props) => {
|
||||||
|
const value = props.row.original.hen_house;
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hen_house_standard',
|
||||||
|
header: 'Hen House Standar',
|
||||||
|
cell: (props) => {
|
||||||
|
const value =
|
||||||
|
props.row.original.project_flock?.production_standart
|
||||||
|
?.hen_house_std;
|
||||||
|
return (
|
||||||
|
<div className='text-center text-gray-600'>
|
||||||
|
{value !== null && value !== undefined
|
||||||
|
? `${value.toFixed(2)}%`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status Approval',
|
header: 'Status Approval',
|
||||||
@@ -728,21 +1013,6 @@ const RecordingTable = () => {
|
|||||||
approvalHistoryModal.openModal();
|
approvalHistoryModal.openModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (action: string) => {
|
|
||||||
switch (action) {
|
|
||||||
case 'APPROVED':
|
|
||||||
return 'Disetujui';
|
|
||||||
case 'REJECTED':
|
|
||||||
return 'Ditolak';
|
|
||||||
case 'CREATED':
|
|
||||||
return 'Dibuat';
|
|
||||||
case 'UPDATED':
|
|
||||||
return 'Diperbarui';
|
|
||||||
default:
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant='soft'
|
variant='soft'
|
||||||
@@ -753,7 +1023,7 @@ const RecordingTable = () => {
|
|||||||
}}
|
}}
|
||||||
onClick={openApprovalHistory}
|
onClick={openApprovalHistory}
|
||||||
>
|
>
|
||||||
{getStatusText(approval.action)}
|
{approval.step_name || approval.action}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -872,14 +1142,15 @@ const RecordingTable = () => {
|
|||||||
'mb-20':
|
'mb-20':
|
||||||
isResponseSuccess(recordings) && recordings?.data?.length === 0,
|
isResponseSuccess(recordings) && recordings?.data?.length === 0,
|
||||||
}),
|
}),
|
||||||
tableWrapperClassName: 'overflow-x-auto min-h-full!',
|
tableWrapperClassName: 'overflow-x-auto',
|
||||||
tableClassName: 'font-inter w-full table-auto min-h-full!',
|
tableClassName: 'w-full table-auto text-sm',
|
||||||
headerRowClassName: 'border-b border-b-gray-200',
|
headerRowClassName: 'border-b border-b-gray-200',
|
||||||
headerColumnClassName:
|
headerColumnClassName:
|
||||||
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
|
'px-4 py-3 text-xs font-semibold text-gray-500 whitespace-nowrap border-l border-l-gray-200 border-r border-r-gray-200 border-t border-t-gray-200 border-gray-200 border-b-0',
|
||||||
bodyRowClassName: 'border-b border-b-gray-200',
|
bodyRowClassName:
|
||||||
|
'hover:bg-gray-50 transition-colors border-b border-gray-200 first:border-t first:border-t-gray-200 border-l border-l-gray-200 border-r border-r-gray-200',
|
||||||
bodyColumnClassName:
|
bodyColumnClassName:
|
||||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
'px-4 py-3 text-xs text-gray-900 whitespace-nowrap',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ import {
|
|||||||
} from '@/types/api/production/recording';
|
} from '@/types/api/production/recording';
|
||||||
|
|
||||||
type RecordingGrowingFormSchemaType = {
|
type RecordingGrowingFormSchemaType = {
|
||||||
|
record_date: string;
|
||||||
|
location?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
location_id: number;
|
||||||
|
project_flock?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
project_flock_id: number;
|
||||||
|
kandang?: {
|
||||||
|
value: number;
|
||||||
|
label: string;
|
||||||
|
} | null;
|
||||||
|
kandang_id: number;
|
||||||
project_flock_kandang: {
|
project_flock_kandang: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,16 +33,16 @@ type RecordingGrowingFormSchemaType = {
|
|||||||
qty: number | string;
|
qty: number | string;
|
||||||
}[];
|
}[];
|
||||||
depletions: {
|
depletions: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
type RecordingLayingFormSchemaType = RecordingGrowingFormSchemaType & {
|
||||||
eggs: {
|
eggs: {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
weight: number | string;
|
weight?: number | string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,14 +52,14 @@ export type StockSchema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DepletionSchema = {
|
export type DepletionSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EggSchema = {
|
export type EggSchema = {
|
||||||
product_warehouse_id: number;
|
product_warehouse_id?: number;
|
||||||
qty: number | string;
|
qty?: number | string;
|
||||||
weight: number | string;
|
weight?: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
||||||
@@ -59,32 +75,51 @@ const StockObjectSchema: Yup.ObjectSchema<StockSchema> = Yup.object({
|
|||||||
|
|
||||||
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
const DepletionObjectSchema: Yup.ObjectSchema<DepletionSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.number()
|
||||||
.required('Produk depletions wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Produk depletions wajib diisi!')
|
.typeError('Depletions harus berupa angka!'),
|
||||||
.typeError('Produk depletions harus berupa angka!'),
|
|
||||||
qty: Yup.number()
|
qty: Yup.number()
|
||||||
.required('Jumlah depletions wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Jumlah depletions minimal 1!')
|
|
||||||
.typeError('Jumlah depletions harus berupa angka!'),
|
.typeError('Jumlah depletions harus berupa angka!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
const EggObjectSchema: Yup.ObjectSchema<EggSchema> = Yup.object({
|
||||||
product_warehouse_id: Yup.number()
|
product_warehouse_id: Yup.number()
|
||||||
.required('Kondisi telur wajib diisi!')
|
.optional()
|
||||||
.min(1, 'Kondisi telur wajib diisi!')
|
|
||||||
.typeError('Kondisi telur harus berupa angka!'),
|
.typeError('Kondisi telur harus berupa angka!'),
|
||||||
qty: Yup.number()
|
qty: Yup.number().optional().typeError('Jumlah telur harus berupa angka!'),
|
||||||
.required('Jumlah telur wajib diisi!')
|
weight: Yup.number().optional().typeError('Berat telur harus berupa angka!'),
|
||||||
.min(1, 'Jumlah telur tidak boleh 0!')
|
|
||||||
.typeError('Jumlah telur harus berupa angka!'),
|
|
||||||
weight: Yup.number()
|
|
||||||
.required('Berat telur wajib diisi!')
|
|
||||||
.min(1, 'Berat telur minimal 1 gram!')
|
|
||||||
.typeError('Berat telur harus berupa angka!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSchemaType> =
|
||||||
Yup.object({
|
Yup.object({
|
||||||
|
record_date: Yup.string()
|
||||||
|
.required('Tanggal recording wajib diisi!')
|
||||||
|
.min(1, 'Tanggal recording wajib diisi!')
|
||||||
|
.typeError('Tanggal recording wajib diisi!'),
|
||||||
|
location: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
location_id: Yup.number()
|
||||||
|
.min(1, 'Lokasi wajib diisi!')
|
||||||
|
.required('Lokasi wajib diisi!')
|
||||||
|
.typeError('Lokasi wajib diisi!'),
|
||||||
|
project_flock: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
project_flock_id: Yup.number()
|
||||||
|
.min(1, 'Project flock wajib diisi!')
|
||||||
|
.required('Project flock wajib diisi!')
|
||||||
|
.typeError('Project flock wajib diisi!'),
|
||||||
|
kandang: Yup.object({
|
||||||
|
value: Yup.number().min(1).required(),
|
||||||
|
label: Yup.string().required(),
|
||||||
|
}).nullable(),
|
||||||
|
kandang_id: Yup.number()
|
||||||
|
.min(1, 'Kandang wajib diisi!')
|
||||||
|
.required('Kandang wajib diisi!')
|
||||||
|
.typeError('Kandang wajib diisi!'),
|
||||||
project_flock_kandang: Yup.object({
|
project_flock_kandang: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
@@ -100,7 +135,7 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
|||||||
.required('Project Flock Kandang wajib diisi!')
|
.required('Project Flock Kandang wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
'not-already-recorded',
|
'not-already-recorded',
|
||||||
'Project Flock ini sudah direcord hari ini!',
|
'Project Flock ini sudah direcord pada tanggal tersebut!',
|
||||||
function (value) {
|
function (value) {
|
||||||
const recordedProjectFlockIds = this.options.context
|
const recordedProjectFlockIds = this.options.context
|
||||||
?.recordedProjectFlockIds as Set<number>;
|
?.recordedProjectFlockIds as Set<number>;
|
||||||
@@ -119,18 +154,12 @@ export const RecordingGrowingFormSchema: Yup.ObjectSchema<RecordingGrowingFormSc
|
|||||||
.of(StockObjectSchema)
|
.of(StockObjectSchema)
|
||||||
.min(1, 'Minimal harus ada 1 data stok!')
|
.min(1, 'Minimal harus ada 1 data stok!')
|
||||||
.required('Data stok wajib diisi!'),
|
.required('Data stok wajib diisi!'),
|
||||||
depletions: Yup.array()
|
depletions: Yup.array().of(DepletionObjectSchema).default([]),
|
||||||
.of(DepletionObjectSchema)
|
|
||||||
.min(1, 'Minimal harus ada 1 data depletions!')
|
|
||||||
.required('Data depletions wajib diisi!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
export const RecordingLayingFormSchema: Yup.ObjectSchema<RecordingLayingFormSchemaType> =
|
||||||
RecordingGrowingFormSchema.shape({
|
RecordingGrowingFormSchema.shape({
|
||||||
eggs: Yup.array()
|
eggs: Yup.array().of(EggObjectSchema).default([]),
|
||||||
.of(EggObjectSchema)
|
|
||||||
.min(1, 'Minimal harus ada 1 data telur!')
|
|
||||||
.required('Data telur wajib diisi!'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const UpdateRecordingGrowingFormSchema =
|
export const UpdateRecordingGrowingFormSchema =
|
||||||
@@ -179,6 +208,15 @@ type RecordingFormData = Partial<Recording> & {
|
|||||||
export const getRecordingGrowingFormInitialValues = (
|
export const getRecordingGrowingFormInitialValues = (
|
||||||
initialValues?: RecordingFormData
|
initialValues?: RecordingFormData
|
||||||
): RecordingGrowingFormValues => ({
|
): RecordingGrowingFormValues => ({
|
||||||
|
record_date: initialValues?.record_datetime
|
||||||
|
? new Date(initialValues.record_datetime).toISOString().split('T')[0]
|
||||||
|
: new Date().toISOString().split('T')[0],
|
||||||
|
location: null,
|
||||||
|
location_id: 0,
|
||||||
|
project_flock: null,
|
||||||
|
project_flock_id: 0,
|
||||||
|
kandang: null,
|
||||||
|
kandang_id: 0,
|
||||||
project_flock_kandang: initialValues?.project_flock_kandang_id
|
project_flock_kandang: initialValues?.project_flock_kandang_id
|
||||||
? {
|
? {
|
||||||
value: initialValues.project_flock_kandang_id,
|
value: initialValues.project_flock_kandang_id,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -179,12 +179,16 @@ const TransferToLayingsTable = () => {
|
|||||||
setInputValue: setFlockSourceInputValue,
|
setInputValue: setFlockSourceInputValue,
|
||||||
options: flockSourceOptions,
|
options: flockSourceOptions,
|
||||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
hasMore: hasMoreFlockSource,
|
||||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setFlockDestinationInputValue,
|
setInputValue: setFlockDestinationInputValue,
|
||||||
options: flockDestinationOptions,
|
options: flockDestinationOptions,
|
||||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
hasMore: hasMoreFlockDestination,
|
||||||
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
} = useSelect<Flock>(FlockApi.basePath, 'id', 'name');
|
||||||
|
|
||||||
// Flocks value
|
// Flocks value
|
||||||
@@ -595,6 +599,7 @@ const TransferToLayingsTable = () => {
|
|||||||
value={selectedFlockSource}
|
value={selectedFlockSource}
|
||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
onInputChange={setFlockSourceInputValue}
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
@@ -608,6 +613,7 @@ const TransferToLayingsTable = () => {
|
|||||||
value={selectedFlockDestination}
|
value={selectedFlockDestination}
|
||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
onInputChange={setFlockDestinationInputValue}
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
isClearable
|
isClearable
|
||||||
className={{
|
className={{
|
||||||
wrapper: 'col-span-12 sm:col-span-3',
|
wrapper: 'col-span-12 sm:col-span-3',
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ const TransferToLayingForm = ({
|
|||||||
options: flockSourceOptions,
|
options: flockSourceOptions,
|
||||||
isLoadingOptions: isLoadingFlockSourceOptions,
|
isLoadingOptions: isLoadingFlockSourceOptions,
|
||||||
rawData: flockSources,
|
rawData: flockSources,
|
||||||
|
loadMore: loadMoreFlockSource,
|
||||||
|
hasMore: hasMoreFlockSource,
|
||||||
} = useSelect<ProjectFlock>(
|
} = useSelect<ProjectFlock>(
|
||||||
'/production/project-flocks',
|
'/production/project-flocks',
|
||||||
'id',
|
'id',
|
||||||
@@ -360,6 +362,8 @@ const TransferToLayingForm = ({
|
|||||||
options: flockDestinationOptions,
|
options: flockDestinationOptions,
|
||||||
isLoadingOptions: isLoadingFlockDestinationOptions,
|
isLoadingOptions: isLoadingFlockDestinationOptions,
|
||||||
rawData: flockDestinations,
|
rawData: flockDestinations,
|
||||||
|
loadMore: loadMoreFlockDestination,
|
||||||
|
hasMore: hasMoreFlockDestination,
|
||||||
} = useSelect<ProjectFlock>(
|
} = useSelect<ProjectFlock>(
|
||||||
'/production/project-flocks',
|
'/production/project-flocks',
|
||||||
'id',
|
'id',
|
||||||
@@ -573,6 +577,7 @@ const TransferToLayingForm = ({
|
|||||||
onChange={flockSourceChangeHandler}
|
onChange={flockSourceChangeHandler}
|
||||||
isLoading={isLoadingFlockSourceOptions}
|
isLoading={isLoadingFlockSourceOptions}
|
||||||
onInputChange={setFlockSourceInputValue}
|
onInputChange={setFlockSourceInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockSource}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockSource &&
|
formik.touched.flockSource &&
|
||||||
Boolean(typeof formik.errors.flockSource === 'string')
|
Boolean(typeof formik.errors.flockSource === 'string')
|
||||||
@@ -591,6 +596,7 @@ const TransferToLayingForm = ({
|
|||||||
onChange={flockDestinationChangeHandler}
|
onChange={flockDestinationChangeHandler}
|
||||||
isLoading={isLoadingFlockDestinationOptions}
|
isLoading={isLoadingFlockDestinationOptions}
|
||||||
onInputChange={setFlockDestinationInputValue}
|
onInputChange={setFlockDestinationInputValue}
|
||||||
|
onMenuScrollToBottom={loadMoreFlockDestination}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.flockDestination &&
|
formik.touched.flockDestination &&
|
||||||
Boolean(typeof formik.errors.flockDestination === 'string')
|
Boolean(typeof formik.errors.flockDestination === 'string')
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ import DateInput from '@/components/input/DateInput';
|
|||||||
import { LocationApi } from '@/services/api/master-data';
|
import { LocationApi } from '@/services/api/master-data';
|
||||||
import { ProjectFlockApi } from '@/services/api/production';
|
import { ProjectFlockApi } from '@/services/api/production';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import {
|
||||||
|
ProjectFlockKandangLookup,
|
||||||
|
ProjectFlock,
|
||||||
|
} from '@/types/api/production/project-flock';
|
||||||
import {
|
import {
|
||||||
getStatusColor,
|
getStatusColor,
|
||||||
getStatusIndicatorColor,
|
getStatusIndicatorColor,
|
||||||
@@ -229,63 +232,37 @@ const UniformityTable = () => {
|
|||||||
useState<number | undefined>(undefined);
|
useState<number | undefined>(undefined);
|
||||||
const [filterStartDate, setFilterStartDate] = useState('');
|
const [filterStartDate, setFilterStartDate] = useState('');
|
||||||
const [filterEndDate, setFilterEndDate] = useState('');
|
const [filterEndDate, setFilterEndDate] = useState('');
|
||||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
const [filterProjectFlockLocationId, setFilterProjectFlockLocationId] =
|
||||||
|
useState<string>('');
|
||||||
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
const [filterErrors, setFilterErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setInputValue: setFilterLocationInputValue,
|
setInputValue: setFilterLocationInputValue,
|
||||||
options: filterLocationOptions,
|
options: filterLocationOptions,
|
||||||
isLoadingOptions: isLoadingFilterLocations,
|
isLoadingOptions: isLoadingFilterLocations,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
loadMore: loadMoreFilterLocations,
|
||||||
limit: '100',
|
hasMore: hasMoreFilterLocations,
|
||||||
});
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
|
// ===== FETCH PROJECT FLOCKS DATA FOR FILTER =====
|
||||||
const filterProjectFlocksUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: projectFlockSearchValue || '',
|
|
||||||
limit: '100',
|
|
||||||
});
|
|
||||||
if (filterLocation) {
|
|
||||||
params.append('location_id', filterLocation.value.toString());
|
|
||||||
}
|
|
||||||
return `${ProjectFlockApi.basePath}?${params.toString()}`;
|
|
||||||
}, [projectFlockSearchValue, filterLocation]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: filterProjectFlocksData,
|
setInputValue: setFilterProjectFlockSearchValue,
|
||||||
isLoading: isLoadingFilterProjectFlocks,
|
options: filterProjectFlockOptions,
|
||||||
} = useSWR(filterProjectFlocksUrl, ProjectFlockApi.getAllFetcher);
|
rawData: filterProjectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingFilterProjectFlocks,
|
||||||
const filterProjectFlocksDataList = useMemo(
|
loadMore: loadMoreFilterProjectFlocks,
|
||||||
() =>
|
hasMore: hasMoreFilterProjectFlocks,
|
||||||
isResponseSuccess(filterProjectFlocksData)
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
? filterProjectFlocksData.data
|
location_id: filterProjectFlockLocationId,
|
||||||
: undefined,
|
});
|
||||||
[filterProjectFlocksData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filterProjectFlockOptions = useMemo(() => {
|
|
||||||
let options: OptionType[] = [];
|
|
||||||
|
|
||||||
if (isResponseSuccess(filterProjectFlocksData)) {
|
|
||||||
const flockOptions =
|
|
||||||
filterProjectFlocksData?.data.map((projectFlock) => ({
|
|
||||||
value: projectFlock.id,
|
|
||||||
label: projectFlock.flock_name || '',
|
|
||||||
})) || [];
|
|
||||||
options = options.concat(flockOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [filterProjectFlocksData]);
|
|
||||||
|
|
||||||
// ===== KANDANG OPTIONS FOR FILTER =====
|
// ===== KANDANG OPTIONS FOR FILTER =====
|
||||||
const filterKandangOptions = useMemo(() => {
|
const filterKandangOptions = useMemo(() => {
|
||||||
let options: OptionType[] = [];
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
if (filterProjectFlock && filterProjectFlocksDataList) {
|
if (filterProjectFlock && isResponseSuccess(filterProjectFlocksRawData)) {
|
||||||
const selectedProjectFlockData = filterProjectFlocksDataList.find(
|
const data = filterProjectFlocksRawData.data as unknown as ProjectFlock[];
|
||||||
|
const selectedProjectFlockData = data.find(
|
||||||
(pf) => pf.id === filterProjectFlock.value
|
(pf) => pf.id === filterProjectFlock.value
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -301,7 +278,7 @@ const UniformityTable = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}, [filterProjectFlock, filterProjectFlocksDataList]);
|
}, [filterProjectFlock, filterProjectFlocksRawData]);
|
||||||
|
|
||||||
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
// ===== PROJECT FLOCK KANDANG LOOKUP =====
|
||||||
const projectFlockKandangLookupUrl = useMemo(() => {
|
const projectFlockKandangLookupUrl = useMemo(() => {
|
||||||
@@ -394,9 +371,13 @@ const UniformityTable = () => {
|
|||||||
// ===== FILTER HANDLERS =====
|
// ===== FILTER HANDLERS =====
|
||||||
const handleFilterLocationChange = useCallback(
|
const handleFilterLocationChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
setFilterLocation(val as OptionType | null);
|
const location = val as OptionType | null;
|
||||||
|
setFilterLocation(location);
|
||||||
setFilterProjectFlock(null);
|
setFilterProjectFlock(null);
|
||||||
setFilterKandang(null);
|
setFilterKandang(null);
|
||||||
|
setFilterProjectFlockLocationId(
|
||||||
|
location ? location.value.toString() : ''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -1206,6 +1187,7 @@ const UniformityTable = () => {
|
|||||||
options={filterLocationOptions}
|
options={filterLocationOptions}
|
||||||
onInputChange={setFilterLocationInputValue}
|
onInputChange={setFilterLocationInputValue}
|
||||||
isLoading={isLoadingFilterLocations}
|
isLoading={isLoadingFilterLocations}
|
||||||
|
onMenuScrollToBottom={loadMoreFilterLocations}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
{filterErrors.location && (
|
{filterErrors.location && (
|
||||||
@@ -1225,8 +1207,9 @@ const UniformityTable = () => {
|
|||||||
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
|
setFilterErrors((prev) => ({ ...prev, project_flock: '' }));
|
||||||
}}
|
}}
|
||||||
options={filterProjectFlockOptions}
|
options={filterProjectFlockOptions}
|
||||||
onInputChange={setProjectFlockSearchValue}
|
onInputChange={setFilterProjectFlockSearchValue}
|
||||||
isLoading={isLoadingFilterProjectFlocks}
|
isLoading={isLoadingFilterProjectFlocks}
|
||||||
|
onMenuScrollToBottom={loadMoreFilterProjectFlocks}
|
||||||
isDisabled={!filterLocation}
|
isDisabled={!filterLocation}
|
||||||
className={{ wrapper: 'w-full' }}
|
className={{ wrapper: 'w-full' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Badge from '../../../../Badge';
|
import Badge from '@/components/Badge';
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { formatNumber } from '@/lib/helper';
|
import { formatNumber } from '@/lib/helper';
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ import {
|
|||||||
VerifyUniformityPayload,
|
VerifyUniformityPayload,
|
||||||
} from '@/types/api/production/uniformity';
|
} from '@/types/api/production/uniformity';
|
||||||
import { type BaseApiResponse } from '@/types/api/api-general';
|
import { type BaseApiResponse } from '@/types/api/api-general';
|
||||||
import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock';
|
import {
|
||||||
|
ProjectFlockKandangLookup,
|
||||||
|
ProjectFlock,
|
||||||
|
} from '@/types/api/production/project-flock';
|
||||||
import { Kandang } from '@/types/api/master-data/kandang';
|
import { Kandang } from '@/types/api/master-data/kandang';
|
||||||
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
import UniformityPreviewForm from '@/components/pages/production/uniformity/form/UniformityPreviewForm';
|
||||||
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
import UniformityResultForm from '@/components/pages/production/uniformity/form/UniformityResultForm';
|
||||||
@@ -88,7 +91,9 @@ const UniformityForm = ({
|
|||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const [projectFlockSearchValue, setProjectFlockSearchValue] = useState('');
|
const [selectedProjectFlockLocationId, setSelectedProjectFlockLocationId] =
|
||||||
|
useState<string>('');
|
||||||
|
|
||||||
const [selectedProjectFlock, setSelectedProjectFlock] =
|
const [selectedProjectFlock, setSelectedProjectFlock] =
|
||||||
useState<OptionType | null>(null);
|
useState<OptionType | null>(null);
|
||||||
|
|
||||||
@@ -100,50 +105,21 @@ const UniformityForm = ({
|
|||||||
setInputValue: setLocationSelectInputValue,
|
setInputValue: setLocationSelectInputValue,
|
||||||
options: locationOptions,
|
options: locationOptions,
|
||||||
isLoadingOptions: isLoadingLocations,
|
isLoadingOptions: isLoadingLocations,
|
||||||
} = useSelect(LocationApi.basePath, 'id', 'name', 'search', {
|
loadMore: loadMoreLocations,
|
||||||
page: '1',
|
hasMore: hasMoreLocations,
|
||||||
limit: '100',
|
} = useSelect(LocationApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
setInputValue: setProjectFlockSearchValue,
|
||||||
|
options: projectFlockOptions,
|
||||||
|
rawData: projectFlocksRawData,
|
||||||
|
isLoadingOptions: isLoadingProjectFlocks,
|
||||||
|
loadMore: loadMoreProjectFlocks,
|
||||||
|
hasMore: hasMoreProjectFlocks,
|
||||||
|
} = useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', 'search', {
|
||||||
|
location_id: selectedProjectFlockLocationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FETCH PROJECT FLOCKS DATA =====
|
|
||||||
const projectFlocksUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: projectFlockSearchValue || '',
|
|
||||||
page: '1',
|
|
||||||
limit: '100',
|
|
||||||
});
|
|
||||||
if (selectedLocation) {
|
|
||||||
params.append('location_id', selectedLocation.value.toString());
|
|
||||||
}
|
|
||||||
return `${ProjectFlockApi.basePath}?${params.toString()}`;
|
|
||||||
}, [projectFlockSearchValue, selectedLocation]);
|
|
||||||
|
|
||||||
const { data: projectFlocksData, isLoading: isLoadingProjectFlocks } = useSWR(
|
|
||||||
projectFlocksUrl,
|
|
||||||
ProjectFlockApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const projectFlocksDataList =
|
|
||||||
projectFlocksData?.status === 'success'
|
|
||||||
? projectFlocksData.data
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// ===== PROJECT FLOCK OPTIONS =====
|
|
||||||
const projectFlockOptions = useMemo(() => {
|
|
||||||
let options: OptionType[] = [];
|
|
||||||
|
|
||||||
if (isResponseSuccess(projectFlocksData)) {
|
|
||||||
const flockOptions =
|
|
||||||
projectFlocksData?.data.map((projectFlock) => ({
|
|
||||||
value: projectFlock.id,
|
|
||||||
label: projectFlock.flock_name || '',
|
|
||||||
})) || [];
|
|
||||||
options = options.concat(flockOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}, [projectFlocksData]);
|
|
||||||
|
|
||||||
// ===== APPROVED PROJECT FLOCK KANDANGS =====
|
// ===== APPROVED PROJECT FLOCK KANDANGS =====
|
||||||
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
const approvedProjectFlockKandangsUrl = useMemo(() => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -168,8 +144,9 @@ const UniformityForm = ({
|
|||||||
const kandangOptions = useMemo(() => {
|
const kandangOptions = useMemo(() => {
|
||||||
let options: OptionType[] = [];
|
let options: OptionType[] = [];
|
||||||
|
|
||||||
if (selectedProjectFlock && projectFlocksDataList) {
|
if (selectedProjectFlock && isResponseSuccess(projectFlocksRawData)) {
|
||||||
const selectedProjectFlockData = projectFlocksDataList.find(
|
const data = projectFlocksRawData.data as unknown as ProjectFlock[];
|
||||||
|
const selectedProjectFlockData = data.find(
|
||||||
(pf) => pf.id === selectedProjectFlock.value
|
(pf) => pf.id === selectedProjectFlock.value
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -196,7 +173,7 @@ const UniformityForm = ({
|
|||||||
return options;
|
return options;
|
||||||
}, [
|
}, [
|
||||||
selectedProjectFlock,
|
selectedProjectFlock,
|
||||||
projectFlocksDataList,
|
projectFlocksRawData,
|
||||||
approvedProjectFlockKandangs,
|
approvedProjectFlockKandangs,
|
||||||
formType,
|
formType,
|
||||||
]);
|
]);
|
||||||
@@ -313,6 +290,10 @@ const UniformityForm = ({
|
|||||||
formik.setFieldValue('location_id', locationId);
|
formik.setFieldValue('location_id', locationId);
|
||||||
|
|
||||||
setSelectedLocation(location);
|
setSelectedLocation(location);
|
||||||
|
setSelectedProjectFlock(null);
|
||||||
|
setSelectedProjectFlockLocationId(
|
||||||
|
location ? location.value.toString() : ''
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -513,6 +494,7 @@ const UniformityForm = ({
|
|||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.location_id && Boolean(formik.errors.location_id)
|
formik.touched.location_id && Boolean(formik.errors.location_id)
|
||||||
}
|
}
|
||||||
@@ -530,6 +512,7 @@ const UniformityForm = ({
|
|||||||
options={projectFlockOptions}
|
options={projectFlockOptions}
|
||||||
onInputChange={setProjectFlockSearchValue}
|
onInputChange={setProjectFlockSearchValue}
|
||||||
isLoading={isLoadingProjectFlocks}
|
isLoading={isLoadingProjectFlocks}
|
||||||
|
onMenuScrollToBottom={loadMoreProjectFlocks}
|
||||||
isDisabled={!formik.values.location_id}
|
isDisabled={!formik.values.location_id}
|
||||||
isError={
|
isError={
|
||||||
formik.touched.project_flock_id &&
|
formik.touched.project_flock_id &&
|
||||||
|
|||||||
@@ -156,8 +156,11 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
setInputValue: setExpeditionsSelectInputValue,
|
setInputValue: setExpeditionsSelectInputValue,
|
||||||
options: expeditionVendors,
|
options: expeditionVendors,
|
||||||
isLoadingOptions: isLoadingExpeditions,
|
isLoadingOptions: isLoadingExpeditions,
|
||||||
|
loadMore: loadMoreExpeditions,
|
||||||
|
hasMore: hasMoreExpeditions,
|
||||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||||
category: 'BOP',
|
category: 'BOP',
|
||||||
|
flag: 'EKSPEDISI',
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== FORM CONFIGURATION =====
|
// ===== FORM CONFIGURATION =====
|
||||||
@@ -183,8 +186,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
purchase_item_id: formItem.purchase_item_id || 0,
|
purchase_item_id: formItem.purchase_item_id || 0,
|
||||||
received_date: formItem.received_date || '',
|
received_date: formItem.received_date || '',
|
||||||
travel_number: formItem.travel_number || '',
|
travel_number: formItem.travel_number || '',
|
||||||
vehicle_number: formItem.vehicle_number || '',
|
vehicle_number: formItem.vehicle_number || null,
|
||||||
expedition_vendor_id: formItem.expedition_vendor_id || 0,
|
expedition_vendor_id: formItem.expedition_vendor_id || null,
|
||||||
received_qty:
|
received_qty:
|
||||||
typeof formItem.received_qty === 'string'
|
typeof formItem.received_qty === 'string'
|
||||||
? parseFloat(formItem.received_qty) || 0
|
? parseFloat(formItem.received_qty) || 0
|
||||||
@@ -192,10 +195,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
transport_per_item:
|
transport_per_item:
|
||||||
typeof formItem.transport_per_item === 'string'
|
typeof formItem.transport_per_item === 'string'
|
||||||
? parseFloat(formItem.transport_per_item) || 0
|
? parseFloat(formItem.transport_per_item) || 0
|
||||||
: formItem.transport_per_item || 0,
|
: formItem.transport_per_item || null,
|
||||||
};
|
};
|
||||||
}) || [],
|
}) || [],
|
||||||
travel_documents: values.travel_documents || [],
|
travel_documents:
|
||||||
|
values.travel_documents
|
||||||
|
?.filter((file): file is File => file instanceof File)
|
||||||
|
.filter(Boolean) || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -403,22 +409,13 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
Dokumen Surat Jalan
|
Dokumen Surat Jalan
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>Nomor Kendaraan</th>
|
||||||
Nomor Kendaraan
|
<th>Vendor Ekspedisi</th>
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
Vendor Ekspedisi
|
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
<th>
|
||||||
Jumlah Diterima
|
Jumlah Diterima
|
||||||
<span className='text-error'>*</span>
|
<span className='text-error'>*</span>
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>Transport/Item</th>
|
||||||
Transport/Item
|
|
||||||
<span className='text-error'>*</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -536,7 +533,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<TextInput
|
<TextInput
|
||||||
required
|
|
||||||
name={`items.${idx}.vehicle_number`}
|
name={`items.${idx}.vehicle_number`}
|
||||||
type='text'
|
type='text'
|
||||||
value={formItem?.vehicle_number || ''}
|
value={formItem?.vehicle_number || ''}
|
||||||
@@ -562,7 +558,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
required
|
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
value={formItem?.expedition_vendor}
|
value={formItem?.expedition_vendor}
|
||||||
key={`expedition-vendor-${idx}`}
|
key={`expedition-vendor-${idx}`}
|
||||||
@@ -570,6 +565,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
expeditionVendorChangeHandler(idx, val)
|
expeditionVendorChangeHandler(idx, val)
|
||||||
}
|
}
|
||||||
options={getExpeditionVendorOptions()}
|
options={getExpeditionVendorOptions()}
|
||||||
|
isLoading={isLoadingExpeditions}
|
||||||
|
onMenuScrollToBottom={loadMoreExpeditions}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(idx, 'expedition_vendor_id')
|
isRepeaterInputError(idx, 'expedition_vendor_id')
|
||||||
.isError
|
.isError
|
||||||
@@ -629,7 +626,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
required
|
|
||||||
name={`items.${idx}.transport_per_item`}
|
name={`items.${idx}.transport_per_item`}
|
||||||
value={formItem?.transport_per_item || ''}
|
value={formItem?.transport_per_item || ''}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -680,7 +676,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
|||||||
|
|
||||||
<div className={'col-span-2 my-2'}>
|
<div className={'col-span-2 my-2'}>
|
||||||
<FileInput
|
<FileInput
|
||||||
required
|
|
||||||
name='travel_documents'
|
name='travel_documents'
|
||||||
label='Dokumen Surat Jalan'
|
label='Dokumen Surat Jalan'
|
||||||
accept='.pdf,.jpg,.jpeg,.png'
|
accept='.pdf,.jpg,.jpeg,.png'
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
|||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
vehicle_number: string;
|
vehicle_number?: string | null;
|
||||||
expedition_vendor?: {
|
expedition_vendor?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
expedition_vendor_id: number;
|
expedition_vendor_id?: number | null;
|
||||||
received_qty: number | string;
|
received_qty: number | string;
|
||||||
transport_per_item: number | string;
|
transport_per_item?: number | string | null;
|
||||||
}[];
|
}[];
|
||||||
travel_documents: File[];
|
travel_documents?: (File | null | undefined)[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseStaffApprovalItemSchema = {
|
export type PurchaseStaffApprovalItemSchema = {
|
||||||
@@ -75,14 +75,14 @@ export type PurchaseAcceptApprovalItemSchema = {
|
|||||||
purchase_item_id: number;
|
purchase_item_id: number;
|
||||||
received_date: string;
|
received_date: string;
|
||||||
travel_number: string;
|
travel_number: string;
|
||||||
vehicle_number: string;
|
vehicle_number?: string | null;
|
||||||
expedition_vendor?: {
|
expedition_vendor?: {
|
||||||
value: number;
|
value: number;
|
||||||
label: string;
|
label: string;
|
||||||
} | null;
|
} | null;
|
||||||
expedition_vendor_id: number;
|
expedition_vendor_id?: number | null;
|
||||||
received_qty: number | string;
|
received_qty: number | string;
|
||||||
transport_per_item: number | string;
|
transport_per_item?: number | string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PurchaseDeleteItemsSchema = {
|
export type PurchaseDeleteItemsSchema = {
|
||||||
@@ -184,24 +184,19 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
.required('No. Surat jalan wajib diisi!')
|
.required('No. Surat jalan wajib diisi!')
|
||||||
.typeError('No. Surat jalan wajib diisi!'),
|
.typeError('No. Surat jalan wajib diisi!'),
|
||||||
vehicle_number: Yup.string()
|
vehicle_number: Yup.string()
|
||||||
.required('Nomor kendaraan wajib diisi!')
|
.nullable()
|
||||||
.typeError('Nomor kendaraan wajib diisi!'),
|
.optional()
|
||||||
|
.typeError('Nomor kendaraan harus berupa plat nomor!'),
|
||||||
expedition_vendor: Yup.object({
|
expedition_vendor: Yup.object({
|
||||||
value: Yup.number().min(1).required(),
|
value: Yup.number().min(1).required(),
|
||||||
label: Yup.string().required(),
|
label: Yup.string().required(),
|
||||||
}).nullable(),
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
expedition_vendor_id: Yup.number()
|
expedition_vendor_id: Yup.number()
|
||||||
.min(1, 'Vendor ekspedisi wajib diisi!')
|
.nullable()
|
||||||
.required('Vendor ekspedisi wajib diisi!')
|
.optional()
|
||||||
.test(
|
.typeError('Vendor ekspedisi harus berupa angka!'),
|
||||||
'is-valid-expedition-vendor',
|
|
||||||
'Vendor ekspedisi harus dipilih!',
|
|
||||||
function (value) {
|
|
||||||
if (!this.parent.expedition_vendor) return true;
|
|
||||||
return Boolean(value && value > 0);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.typeError('Vendor ekspedisi harus dipilih!'),
|
|
||||||
received_qty: Yup.mixed<string | number>()
|
received_qty: Yup.mixed<string | number>()
|
||||||
.required('Jumlah diterima wajib diisi!')
|
.required('Jumlah diterima wajib diisi!')
|
||||||
.test(
|
.test(
|
||||||
@@ -217,13 +212,14 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
|||||||
)
|
)
|
||||||
.typeError('Jumlah diterima harus berupa angka!'),
|
.typeError('Jumlah diterima harus berupa angka!'),
|
||||||
transport_per_item: Yup.mixed<string | number>()
|
transport_per_item: Yup.mixed<string | number>()
|
||||||
.required('Biaya transport per item wajib diisi!')
|
.nullable()
|
||||||
|
.optional()
|
||||||
.test(
|
.test(
|
||||||
'is-valid-transport-per-item',
|
'is-valid-transport-per-item',
|
||||||
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
||||||
function (value) {
|
function (value) {
|
||||||
if (value === '' || value === null || value === undefined)
|
if (value === '' || value === null || value === undefined)
|
||||||
return false;
|
return true;
|
||||||
const numValue =
|
const numValue =
|
||||||
typeof value === 'string' ? parseFloat(value) : value;
|
typeof value === 'string' ? parseFloat(value) : value;
|
||||||
return !isNaN(numValue) && numValue >= 0;
|
return !isNaN(numValue) && numValue >= 0;
|
||||||
@@ -389,16 +385,17 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
|
|||||||
travel_documents: Yup.array()
|
travel_documents: Yup.array()
|
||||||
.of(
|
.of(
|
||||||
Yup.mixed<File>()
|
Yup.mixed<File>()
|
||||||
.required('Dokumen surat jalan wajib diupload!')
|
.nullable()
|
||||||
|
.optional()
|
||||||
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
.test('fileSize', 'Ukuran dokumen maksimal 5 MB', (value) => {
|
||||||
if (!value) return true;
|
if (!value) return true;
|
||||||
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
if (value instanceof File) return value.size <= 5 * 1024 * 1024;
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.required('Dokumen surat jalan wajib diupload!')
|
.nullable()
|
||||||
.min(1, 'Minimal upload 1 dokumen surat jalan!')
|
.optional()
|
||||||
.typeError('Dokumen surat jalan wajib diupload!'),
|
.typeError('Dokumen surat jalan harus berupa array!'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||||
|
|||||||
@@ -633,8 +633,18 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
|
|
||||||
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
||||||
|
|
||||||
formik.setFieldValue(`items.${idx}.price`, '');
|
if (
|
||||||
formik.setFieldValue(`items.${idx}.total_price`, '');
|
formItem.price !== '' &&
|
||||||
|
formItem.price !== undefined &&
|
||||||
|
formItem.price !== null &&
|
||||||
|
numValue !== '' &&
|
||||||
|
numValue > 0
|
||||||
|
) {
|
||||||
|
const calculatedTotal = Number(formItem.price) * Number(numValue);
|
||||||
|
formik.setFieldValue(`items.${idx}.total_price`, calculatedTotal);
|
||||||
|
} else if (numValue === '') {
|
||||||
|
formik.setFieldValue(`items.${idx}.total_price`, '');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field === 'price' || field === 'total_price') {
|
if (field === 'price' || field === 'total_price') {
|
||||||
@@ -1184,8 +1194,10 @@ const PurchaseOrderStaffApprovalForm = ({
|
|||||||
color='warning'
|
color='warning'
|
||||||
className='px-4'
|
className='px-4'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
formik.setValues(formikInitialValues);
|
if (type === 'add') {
|
||||||
formik.resetForm();
|
formik.setValues(formikInitialValues);
|
||||||
|
formik.resetForm();
|
||||||
|
}
|
||||||
setPurchaseOrderFormErrorMessage('');
|
setPurchaseOrderFormErrorMessage('');
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
onModalClose?.();
|
onModalClose?.();
|
||||||
|
|||||||
@@ -63,11 +63,9 @@ const PurchaseRequestForm = ({
|
|||||||
useState('');
|
useState('');
|
||||||
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
const [formErrorList, setFormErrorList] = useState<string[]>([]);
|
||||||
|
|
||||||
// ===== TYPE DEFINITIONS =====
|
const [selectedArea, setSelectedArea] = useState('');
|
||||||
interface ProductOptionType {
|
const [selectedLocation, setSelectedLocation] = useState('');
|
||||||
value: number;
|
const [disabledLocation, setDisabledLocation] = useState(true);
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== UTILITY FUNCTIONS =====
|
// ===== UTILITY FUNCTIONS =====
|
||||||
const isRepeaterInputError = (
|
const isRepeaterInputError = (
|
||||||
@@ -160,11 +158,35 @@ const PurchaseRequestForm = ({
|
|||||||
isLoadingOptions: isLoadingAreas,
|
isLoadingOptions: isLoadingAreas,
|
||||||
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||||
|
|
||||||
|
const {
|
||||||
|
options: locationOptions,
|
||||||
|
isLoadingOptions: isLoadingLocations,
|
||||||
|
loadMore: loadMoreLocations,
|
||||||
|
hasMore: hasMoreLocations,
|
||||||
|
} = useSelect(LocationApi.basePath, 'id', 'name', '', {
|
||||||
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
inputValue: warehouseSelectInputValue,
|
inputValue: warehouseSelectInputValue,
|
||||||
setInputValue: setWarehouseSelectInputValue,
|
setInputValue: setWarehouseSelectInputValue,
|
||||||
|
options: warehouseOptions,
|
||||||
isLoadingOptions: isLoadingWarehouses,
|
isLoadingOptions: isLoadingWarehouses,
|
||||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
loadMore: loadMoreWarehouses,
|
||||||
|
hasMore: hasMoreWarehouses,
|
||||||
|
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search', {
|
||||||
|
area_id:
|
||||||
|
selectedArea != ''
|
||||||
|
? selectedArea
|
||||||
|
: ((initialValues?.area?.id ?? '') as string),
|
||||||
|
location_id:
|
||||||
|
selectedLocation != ''
|
||||||
|
? selectedLocation
|
||||||
|
: ((initialValues?.location?.id ?? '') as string),
|
||||||
|
});
|
||||||
|
|
||||||
// ===== FORM CONFIGURATION =====
|
// ===== FORM CONFIGURATION =====
|
||||||
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
||||||
@@ -267,70 +289,6 @@ const PurchaseRequestForm = ({
|
|||||||
return data;
|
return data;
|
||||||
}, [supplierData]);
|
}, [supplierData]);
|
||||||
|
|
||||||
const locationsUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
search: locationSelectInputValue,
|
|
||||||
...(formik.values.area_id && formik.values.area_id > 0
|
|
||||||
? { area_id: formik.values.area_id.toString() }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
return `${LocationApi.basePath}?${params.toString()}`;
|
|
||||||
}, [locationSelectInputValue, formik.values.area_id]);
|
|
||||||
|
|
||||||
const { data: locations, isLoading: isLoadingLocations } = useSWR(
|
|
||||||
locationsUrl,
|
|
||||||
LocationApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const locationOptions = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(locations)) return [];
|
|
||||||
return (
|
|
||||||
locations?.data.map((location) => ({
|
|
||||||
value: location.id,
|
|
||||||
label: location.name,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [locations]);
|
|
||||||
|
|
||||||
const warehousesUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams({ search: warehouseSelectInputValue });
|
|
||||||
|
|
||||||
if (formik.values.area_id && formik.values.area_id > 0) {
|
|
||||||
params.append('area_id', formik.values.area_id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formik.values.location_id && formik.values.location_id > 0) {
|
|
||||||
params.append('location_id', formik.values.location_id.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${WarehouseApi.basePath}?${params.toString()}`;
|
|
||||||
}, [
|
|
||||||
warehouseSelectInputValue,
|
|
||||||
formik.values.area_id,
|
|
||||||
formik.values.location_id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { data: warehouses } = useSWR(
|
|
||||||
warehousesUrl,
|
|
||||||
WarehouseApi.getAllFetcher
|
|
||||||
);
|
|
||||||
|
|
||||||
const warehouseOptions = useMemo(() => {
|
|
||||||
if (!isResponseSuccess(warehouses)) return [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
warehouses?.data.map((w) => ({
|
|
||||||
value: w.id,
|
|
||||||
label: w.name,
|
|
||||||
area: w.area?.name,
|
|
||||||
location:
|
|
||||||
'type' in w && (w.type === 'LOKASI' || w.type === 'KANDANG')
|
|
||||||
? w.location?.name
|
|
||||||
: undefined,
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}, [warehouses]);
|
|
||||||
|
|
||||||
const addPurchaseItem = () => {
|
const addPurchaseItem = () => {
|
||||||
const newItems = [
|
const newItems = [
|
||||||
...(formik.values.items || []),
|
...(formik.values.items || []),
|
||||||
@@ -407,6 +365,18 @@ const PurchaseRequestForm = ({
|
|||||||
}
|
}
|
||||||
}, [formik.values.supplier_id]);
|
}, [formik.values.supplier_id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (type !== 'add' && initialValues) {
|
||||||
|
if (initialValues.area?.id) {
|
||||||
|
setSelectedArea(initialValues.area.id.toString());
|
||||||
|
setDisabledLocation(false);
|
||||||
|
}
|
||||||
|
if (initialValues.location?.id) {
|
||||||
|
setSelectedLocation(initialValues.location.id.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [type, initialValues]);
|
||||||
|
|
||||||
// ===== FORM HANDLERS =====
|
// ===== FORM HANDLERS =====
|
||||||
const handleSupplierChange = useCallback(
|
const handleSupplierChange = useCallback(
|
||||||
(val: OptionType | OptionType[] | null) => {
|
(val: OptionType | OptionType[] | null) => {
|
||||||
@@ -445,6 +415,16 @@ const PurchaseRequestForm = ({
|
|||||||
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
||||||
formik.setFieldTouched('area', true);
|
formik.setFieldTouched('area', true);
|
||||||
formik.setFieldValue('area', area);
|
formik.setFieldValue('area', area);
|
||||||
|
|
||||||
|
setSelectedArea((area as OptionType)?.value as string);
|
||||||
|
setSelectedLocation('');
|
||||||
|
const disabled = (area as OptionType)?.value == null;
|
||||||
|
setDisabledLocation(disabled);
|
||||||
|
|
||||||
|
formik.setFieldTouched('location_id', false);
|
||||||
|
formik.setFieldValue('location_id', 0);
|
||||||
|
formik.setFieldTouched('location', false);
|
||||||
|
formik.setFieldValue('location', null);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -456,6 +436,8 @@ const PurchaseRequestForm = ({
|
|||||||
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
||||||
formik.setFieldTouched('location', true);
|
formik.setFieldTouched('location', true);
|
||||||
formik.setFieldValue('location', location);
|
formik.setFieldValue('location', location);
|
||||||
|
|
||||||
|
setSelectedLocation((location as OptionType)?.value as string);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@@ -596,10 +578,15 @@ const PurchaseRequestForm = ({
|
|||||||
placeholder='Pilih Lokasi...'
|
placeholder='Pilih Lokasi...'
|
||||||
value={formik.values.location}
|
value={formik.values.location}
|
||||||
onChange={handleLocationChange}
|
onChange={handleLocationChange}
|
||||||
options={locationOptions}
|
options={
|
||||||
|
selectedArea != '' || initialValues?.area?.id
|
||||||
|
? locationOptions
|
||||||
|
: []
|
||||||
|
}
|
||||||
onInputChange={setLocationSelectInputValue}
|
onInputChange={setLocationSelectInputValue}
|
||||||
isLoading={isLoadingLocations}
|
isLoading={isLoadingLocations}
|
||||||
isDisabled={type === 'detail'}
|
onMenuScrollToBottom={loadMoreLocations}
|
||||||
|
isDisabled={type === 'detail' || disabledLocation}
|
||||||
isClearable={type !== 'detail'}
|
isClearable={type !== 'detail'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -713,6 +700,7 @@ const PurchaseRequestForm = ({
|
|||||||
options={warehouseOptions}
|
options={warehouseOptions}
|
||||||
onInputChange={setWarehouseSelectInputValue}
|
onInputChange={setWarehouseSelectInputValue}
|
||||||
isLoading={isLoadingWarehouses}
|
isLoading={isLoadingWarehouses}
|
||||||
|
onMenuScrollToBottom={loadMoreWarehouses}
|
||||||
isError={
|
isError={
|
||||||
isRepeaterInputError(idx, 'warehouse_id').isError
|
isRepeaterInputError(idx, 'warehouse_id').isError
|
||||||
}
|
}
|
||||||
@@ -732,9 +720,9 @@ const PurchaseRequestForm = ({
|
|||||||
required
|
required
|
||||||
value={item.product ?? undefined}
|
value={item.product ?? undefined}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
const product = val as ProductOptionType | null;
|
const product = val as OptionType | null;
|
||||||
const productId =
|
const productId =
|
||||||
(product as ProductOptionType)?.value || 0;
|
(product as OptionType)?.value || 0;
|
||||||
|
|
||||||
formik.setFieldTouched(
|
formik.setFieldTouched(
|
||||||
`items.${idx}.product`,
|
`items.${idx}.product`,
|
||||||
|
|||||||
@@ -540,31 +540,6 @@ const PurchaseOrderDetail = ({
|
|||||||
accessorKey: 'travel_number',
|
accessorKey: 'travel_number',
|
||||||
cell: (props) => props.row.original.travel_number || '-',
|
cell: (props) => props.row.original.travel_number || '-',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
header: 'Dokumen Surat Jalan',
|
|
||||||
accessorKey: 'travel_document_path',
|
|
||||||
cell: (props) => {
|
|
||||||
const documentPath = props.row.original.travel_document_path;
|
|
||||||
return documentPath ? (
|
|
||||||
<Button
|
|
||||||
color='primary'
|
|
||||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm'
|
|
||||||
href={documentPath}
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon='material-symbols:file-open-outline'
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Lihat Dokumen
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
'-'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
header: 'No. Armada Pengangkut',
|
header: 'No. Armada Pengangkut',
|
||||||
accessorKey: 'vehicle_number',
|
accessorKey: 'vehicle_number',
|
||||||
@@ -588,7 +563,10 @@ const PurchaseOrderDetail = ({
|
|||||||
{
|
{
|
||||||
header: 'Transport /Item',
|
header: 'Transport /Item',
|
||||||
accessorKey: 'transport_per_item',
|
accessorKey: 'transport_per_item',
|
||||||
cell: (props) => formatCurrency(props.getValue() as number),
|
cell: (props) => {
|
||||||
|
const value = props.row.original.transport_per_item;
|
||||||
|
return value ? formatCurrency(value) : formatCurrency(0);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -723,8 +701,8 @@ const PurchaseOrderDetail = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className='text-gray-900 ml-3 break-all'>
|
<span className='text-gray-900 ml-3 break-all'>
|
||||||
:{' '}
|
:{' '}
|
||||||
{purchaseData.items?.[0]?.warehouse?.type === 'LOKASI' &&
|
{purchaseData.items?.[0]?.warehouse &&
|
||||||
purchaseData.items?.[0]?.warehouse?.location?.name
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.name
|
? purchaseData.items[0].warehouse.location.name
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
@@ -905,11 +883,29 @@ const PurchaseOrderDetail = ({
|
|||||||
Informasi Penerimaan Barang
|
Informasi Penerimaan Barang
|
||||||
</h3>
|
</h3>
|
||||||
{canShowPenerimaanBarang && (
|
{canShowPenerimaanBarang && (
|
||||||
<RowDropdownOptions isLast2Rows>
|
<div className='flex items-center gap-2'>
|
||||||
<PenerimaanBarangDropdown
|
{goodsReceiptItems[0]?.travel_document_path && (
|
||||||
onEdit={penerimaanBarangModal.openModal}
|
<Button
|
||||||
/>
|
color='primary'
|
||||||
</RowDropdownOptions>
|
className='w-fit min-w-32 flex items-center justify-start gap-1 p-1.5 text-sm'
|
||||||
|
href={goodsReceiptItems[0].travel_document_path}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon='material-symbols:file-open-outline'
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Lihat Dokumen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<RowDropdownOptions isLast2Rows>
|
||||||
|
<PenerimaanBarangDropdown
|
||||||
|
onEdit={penerimaanBarangModal.openModal}
|
||||||
|
/>
|
||||||
|
</RowDropdownOptions>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='overflow-x-auto'>
|
<div className='overflow-x-auto'>
|
||||||
|
|||||||
@@ -324,12 +324,14 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
PT LUMBUNG TELUR INDONESIA
|
PT LUMBUNG TELUR INDONESIA
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
{purchaseData?.items?.[0]?.warehouse &&
|
||||||
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.name
|
? purchaseData.items[0].warehouse.location.name
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
{purchaseData?.items?.[0]?.warehouse &&
|
||||||
|
'location' in purchaseData.items[0].warehouse
|
||||||
? purchaseData.items[0].warehouse.location.address
|
? purchaseData.items[0].warehouse.location.address
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -434,7 +436,7 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
|||||||
</View>
|
</View>
|
||||||
<View style={pdfStyles.tableCell}>
|
<View style={pdfStyles.tableCell}>
|
||||||
<Text>
|
<Text>
|
||||||
{item.warehouse?.type === 'LOKASI'
|
{item.warehouse && 'location' in item.warehouse
|
||||||
? item.warehouse.location.address
|
? item.warehouse.location.address
|
||||||
: '-'}
|
: '-'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user