mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 05:22:02 +00:00
Merge branch 'development' into feat/FE/US-164/TASK-200-204-205-206-207-expense-realization
This commit is contained in:
+1
-1
@@ -1,3 +1,3 @@
|
||||
npm run format
|
||||
npm run lint
|
||||
npm run build
|
||||
npm run build
|
||||
@@ -0,0 +1,11 @@
|
||||
import PurchaseRequestForm from '@/components/pages/purchase/form/request/PurchaseRequestForm';
|
||||
|
||||
const AddPurchaseRequest = () => {
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
<PurchaseRequestForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPurchaseRequest;
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import PurchaseRequestForm from '@/components/pages/purchase/form/request/PurchaseRequestForm';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
|
||||
const PurchaseEdit = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const purchaseId = searchParams.get('purchaseId');
|
||||
|
||||
const { data: purchase, isLoading: isLoadingPurchase } = useSWR(
|
||||
purchaseId,
|
||||
(id: number) => PurchaseApi.getSingle(id)
|
||||
);
|
||||
|
||||
if (!purchaseId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingPurchase && (!purchase || isResponseError(purchase))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4 flex flex-row justify-center'>
|
||||
{isLoadingPurchase && (
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
)}
|
||||
{!isLoadingPurchase && isResponseSuccess(purchase) && (
|
||||
<PurchaseRequestForm type='edit' initialValues={purchase.data} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseEdit;
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import useSWR from 'swr';
|
||||
import PurchaseOrderDetail from '@/components/pages/purchase/order/PurchaseOrderDetail';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
|
||||
const PurchaseDetail = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const purchaseId = searchParams.get('purchaseId');
|
||||
|
||||
const {
|
||||
data: purchase,
|
||||
isLoading: isLoadingPurchase,
|
||||
mutate: mutatePurchase,
|
||||
} = useSWR(purchaseId, (id: number) => PurchaseApi.getSingle(id));
|
||||
|
||||
if (!purchaseId) {
|
||||
router.back();
|
||||
|
||||
return (
|
||||
<div className='w-full flex flex-row justify-center items-center p-4'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingPurchase && (!purchase || isResponseError(purchase))) {
|
||||
router.replace('/404');
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full p-4'>
|
||||
{isLoadingPurchase && (
|
||||
<div className='w-full flex flex-row justify-center items-center'>
|
||||
<span className='loading loading-spinner loading-xl' />
|
||||
</div>
|
||||
)}
|
||||
{!isLoadingPurchase && isResponseSuccess(purchase) && (
|
||||
<PurchaseOrderDetail
|
||||
type='detail'
|
||||
initialValues={purchase.data}
|
||||
refetchData={mutatePurchase}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseDetail;
|
||||
@@ -0,0 +1,11 @@
|
||||
import SuspenseHelper from '@/components/helper/SuspenseHelper';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) => {
|
||||
return <SuspenseHelper>{children}</SuspenseHelper>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,11 @@
|
||||
import PurchaseTable from '@/components/pages/purchase/PurchaseTable';
|
||||
|
||||
const Purchase = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<PurchaseTable />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Purchase;
|
||||
+127
-33
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes, ReactNode } from 'react';
|
||||
import { HTMLAttributes, ReactNode, useState } from 'react';
|
||||
|
||||
import { cn } from '@/lib/helper';
|
||||
import Image from 'next/image';
|
||||
import Collapse from './Collapse';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
export interface CardProps
|
||||
extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
|
||||
@@ -11,8 +13,13 @@ export interface CardProps
|
||||
subtitle?: string;
|
||||
image?: string;
|
||||
imageAlt?: string;
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
actions?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
collapsible?: boolean;
|
||||
defaultCollapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
className?: {
|
||||
wrapper?: string;
|
||||
image?: string;
|
||||
@@ -21,6 +28,7 @@ export interface CardProps
|
||||
subtitle?: string;
|
||||
actions?: string;
|
||||
footer?: string;
|
||||
collapsible?: string;
|
||||
};
|
||||
variant?: 'default' | 'compact' | 'bordered' | 'shadow' | 'image-full';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
@@ -31,14 +39,27 @@ const Card = ({
|
||||
subtitle,
|
||||
image,
|
||||
imageAlt,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
actions,
|
||||
footer,
|
||||
collapsible,
|
||||
defaultCollapsed = false,
|
||||
onCollapsedChange,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
children,
|
||||
...props
|
||||
}: CardProps) => {
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
|
||||
const handleCollapsedChange = (open: boolean) => {
|
||||
const collapsed = !open;
|
||||
setIsCollapsed(collapsed);
|
||||
onCollapsedChange?.(collapsed);
|
||||
};
|
||||
|
||||
const getCardClasses = () => {
|
||||
const baseClasses = 'card bg-base-100';
|
||||
|
||||
@@ -64,11 +85,31 @@ const Card = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getImageDimensions = () => {
|
||||
if (variant === 'image-full') {
|
||||
return {
|
||||
width: imageWidth || 128,
|
||||
height: imageHeight || 128,
|
||||
};
|
||||
}
|
||||
|
||||
const cardWidths = {
|
||||
sm: 256, // w-64
|
||||
md: 384, // w-96
|
||||
lg: 448, // w-[28rem]
|
||||
};
|
||||
|
||||
return {
|
||||
width: imageWidth || cardWidths[size],
|
||||
height: imageHeight || 192,
|
||||
};
|
||||
};
|
||||
|
||||
const getImageClasses = () => {
|
||||
if (variant === 'image-full') {
|
||||
return cn('w-32 h-32 object-cover', className?.image);
|
||||
return cn('object-cover', className?.image);
|
||||
}
|
||||
return cn('h-48 object-cover', className?.image);
|
||||
return cn('w-full object-cover', className?.image);
|
||||
};
|
||||
|
||||
const getBodyClasses = () => {
|
||||
@@ -103,45 +144,98 @@ const Card = ({
|
||||
return cn('border-t border-base-300 mt-4 pt-4', className?.footer);
|
||||
};
|
||||
|
||||
const renderCardContent = () => {
|
||||
const hasContent = children || actions || footer;
|
||||
|
||||
const titleContent = (
|
||||
<div className='group flex items-center !justify-between w-full'>
|
||||
<div className='flex-1'>
|
||||
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||
</div>
|
||||
{collapsible && (
|
||||
<button
|
||||
onClick={() => handleCollapsedChange(!isCollapsed)}
|
||||
className='btn btn-ghost btn-sm btn-circle'
|
||||
aria-label={isCollapsed ? 'Expand content' : 'Collapse content'}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
isCollapsed
|
||||
? 'material-symbols:expand-more'
|
||||
: 'material-symbols:expand-less'
|
||||
}
|
||||
width={20}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const cardContent = (
|
||||
<div className='space-y-4'>
|
||||
{children}
|
||||
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{image && (
|
||||
<figure>
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt || title || 'Card image'}
|
||||
width={getImageDimensions().width}
|
||||
height={getImageDimensions().height}
|
||||
className={getImageClasses()}
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div className={getBodyClasses()}>
|
||||
{collapsible && hasContent ? (
|
||||
<Collapse
|
||||
variant='default'
|
||||
bordered={false}
|
||||
open={!isCollapsed}
|
||||
onOpenChange={handleCollapsedChange}
|
||||
title={titleContent}
|
||||
titleClassName='w-full cursor-pointer'
|
||||
contentClassName='p-0'
|
||||
fullWidth={true}
|
||||
>
|
||||
{cardContent}
|
||||
</Collapse>
|
||||
) : (
|
||||
<>
|
||||
{(title || subtitle) && (
|
||||
<div className='mb-4'>
|
||||
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||
{subtitle && (
|
||||
<p className={getSubtitleClasses()}>{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasContent && cardContent}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (variant === 'image-full' && image) {
|
||||
return (
|
||||
<div className={getCardClasses()} {...props}>
|
||||
<figure>
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt || title || 'Card image'}
|
||||
className={getImageClasses()}
|
||||
/>
|
||||
</figure>
|
||||
<div className={getBodyClasses()}>
|
||||
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||
{children}
|
||||
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||
</div>
|
||||
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||
{renderCardContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getCardClasses()} {...props}>
|
||||
{image && (
|
||||
<figure>
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt || title || 'Card image'}
|
||||
className={getImageClasses()}
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div className={getBodyClasses()}>
|
||||
{title && <h2 className={getTitleClasses()}>{title}</h2>}
|
||||
{subtitle && <p className={getSubtitleClasses()}>{subtitle}</p>}
|
||||
{children}
|
||||
{actions && <div className={getActionsClasses()}>{actions}</div>}
|
||||
</div>
|
||||
{footer && <div className={getFooterClasses()}>{footer}</div>}
|
||||
{renderCardContent()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,9 @@ export type CollapseProps = {
|
||||
disabled?: boolean;
|
||||
/** Allow only one open at a time by switching to radio input */
|
||||
asRadio?: boolean;
|
||||
/** Force full width instead of auto-fit when collapsed
|
||||
* (Khusus justify-between dan justify-end) */
|
||||
fullWidth?: boolean;
|
||||
/** Extra classnames */
|
||||
className?: string;
|
||||
titleClassName?: string;
|
||||
@@ -44,6 +47,7 @@ export const Collapse = ({
|
||||
bordered,
|
||||
disabled,
|
||||
asRadio = false,
|
||||
fullWidth,
|
||||
className,
|
||||
titleClassName,
|
||||
contentClassName,
|
||||
@@ -68,9 +72,9 @@ export const Collapse = ({
|
||||
'collapse',
|
||||
variant === 'arrow' && 'collapse-arrow',
|
||||
variant === 'plus' && 'collapse-plus',
|
||||
bordered && 'border base-content/20 border-opacity-20 rounded',
|
||||
bordered && 'border base-content/20 border-opacity-20 rounded-box',
|
||||
disabled && 'opacity-60 pointer-events-none',
|
||||
!open && 'w-fit',
|
||||
!fullWidth && !open && 'w-fit',
|
||||
className
|
||||
);
|
||||
|
||||
|
||||
@@ -10,15 +10,19 @@ import {
|
||||
} from 'react';
|
||||
import { cn } from '@/lib/helper';
|
||||
|
||||
export const useModal = () => {
|
||||
export const useModal = (isNestingModal = false) => {
|
||||
const ref = useRef<HTMLDialogElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
ref.current.show();
|
||||
if (isNestingModal) {
|
||||
ref.current.showModal();
|
||||
} else {
|
||||
ref.current.show();
|
||||
}
|
||||
setOpen(true);
|
||||
}, []);
|
||||
}, [isNestingModal]);
|
||||
|
||||
const closeModal = useCallback(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn, formatDate } from '@/lib/helper';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import Modal, { useModal } from '../Modal';
|
||||
import { DateRange, DayPicker, Matcher } from 'react-day-picker';
|
||||
import 'react-day-picker/dist/style.css';
|
||||
import Button from '@/components/Button';
|
||||
import Button from '../Button';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
export interface DateInputProps {
|
||||
@@ -34,6 +34,7 @@ export interface DateInputProps {
|
||||
required?: boolean;
|
||||
isLoading?: boolean;
|
||||
isRange?: boolean;
|
||||
isNestedModal?: boolean; // New prop to indicate if used inside another modal
|
||||
errorMessage?: string;
|
||||
onChange?: ChangeEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
@@ -58,6 +59,7 @@ const DateInput = ({
|
||||
readOnly = false,
|
||||
isLoading = false,
|
||||
isRange = false,
|
||||
isNestedModal = false,
|
||||
}: DateInputProps) => {
|
||||
const [internalError, setInternalError] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<Date | undefined>();
|
||||
@@ -74,7 +76,7 @@ const DateInput = ({
|
||||
? new Date(max.split('/').reverse().join('-'))
|
||||
: undefined;
|
||||
|
||||
const calendarModal = useModal();
|
||||
const calendarModal = useModal(isNestedModal);
|
||||
|
||||
// --- Sync value props ---
|
||||
useEffect(() => {
|
||||
@@ -213,7 +215,7 @@ const DateInput = ({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'input h-12 px-4 py-2 text-base font-normal leading-6 w-full rounded transition-all duration-200 flex items-center border',
|
||||
'input h-12 bg-inherit px-4 py-2 text-base font-normal leading-6 w-full rounded transition-all duration-200 flex items-center border',
|
||||
{
|
||||
'border-error': finalIsError,
|
||||
'border-success': externalValid && !finalIsError,
|
||||
@@ -264,7 +266,7 @@ const DateInput = ({
|
||||
ref={calendarModal.ref}
|
||||
className={{
|
||||
modal: 'rounded',
|
||||
modalBox: `w-fit min-h-${isRange ? '124' : '110'} flex flex-col`,
|
||||
modalBox: `!max-w-max min-h-${isRange ? '124' : '110'} flex flex-col`,
|
||||
}}
|
||||
closeOnBackdrop
|
||||
>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEvent, ChangeEventHandler, useEffect, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import TextArea, { TextAreaProps } from '@/components/input/TextArea';
|
||||
|
||||
interface DebouncedTextAreaProps extends TextAreaProps {
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const DebouncedTextArea = (props: DebouncedTextAreaProps) => {
|
||||
const { delay, onChange } = props;
|
||||
|
||||
const [internalChangeEvent, setInternalChangeEvent] =
|
||||
useState<ChangeEvent<HTMLTextAreaElement>>();
|
||||
const [internalValue, setInternalValue] = useState(props.value);
|
||||
|
||||
const [debouncedChangeEvent] = useDebounce(internalChangeEvent, delay ?? 300);
|
||||
const [debouncedValue] = useDebounce(internalValue, delay ?? 300);
|
||||
|
||||
const internalChangeHandler: ChangeEventHandler<HTMLTextAreaElement> = (
|
||||
e
|
||||
) => {
|
||||
setInternalValue(e.target.value);
|
||||
setInternalChangeEvent(e);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedChangeEvent) {
|
||||
onChange?.(debouncedChangeEvent);
|
||||
}
|
||||
}, [debouncedValue]);
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
{...props}
|
||||
value={internalValue}
|
||||
onChange={internalChangeHandler}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebouncedTextArea;
|
||||
@@ -71,9 +71,8 @@ const InventoryAdjustmentForm = ({
|
||||
Partial<InventoryAdjustmentFormValues>
|
||||
>(() => {
|
||||
return {
|
||||
product_category_id: initialValues?.product_category?.id ?? 0,
|
||||
product_id: initialValues?.product?.id ?? 0,
|
||||
warehouse_id: initialValues?.warehouse?.id ?? 0,
|
||||
product_id: initialValues?.product_warehouse?.product_id ?? 0,
|
||||
warehouse_id: initialValues?.product_warehouse?.warehouse_id ?? 0,
|
||||
product_category: undefined,
|
||||
product: undefined,
|
||||
warehouse: undefined,
|
||||
|
||||
@@ -9,12 +9,10 @@ import { Icon } from '@iconify/react';
|
||||
import { Movement } from '@/types/api/inventory/movement';
|
||||
import { MovementApi } from '@/services/api/inventory';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { OptionType, useSelect } from '@/components/input/SelectInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import Button from '@/components/Button';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import SelectInput from '@/components/input/SelectInput';
|
||||
@@ -52,38 +50,15 @@ const MovementTable = () => {
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
product: '',
|
||||
warehouse: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
product: 'product_id',
|
||||
warehouse: 'warehouse_id',
|
||||
},
|
||||
});
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const {
|
||||
setInputValue: setProductInputValue,
|
||||
options: productOptions,
|
||||
isLoadingOptions: isLoadingProductOptions,
|
||||
} = useSelect<Product>('/products', 'id', 'name');
|
||||
|
||||
const {
|
||||
setInputValue: setWarehouseInputValue,
|
||||
options: warehouseOptions,
|
||||
isLoadingOptions: isLoadingWarehouseOptions,
|
||||
} = useSelect<Warehouse>('/warehouses', 'id', 'name');
|
||||
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [selectedWarehouse, setSelectedWarehouse] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { data: movements, isLoading } = useSWR(
|
||||
`${MovementApi.basePath}${getTableFilterQueryString()}`,
|
||||
MovementApi.getAllFetcher
|
||||
@@ -99,16 +74,6 @@ const MovementTable = () => {
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const productChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedProduct(val as OptionType);
|
||||
updateFilter('product', val ? ((val as OptionType).value as string) : '');
|
||||
};
|
||||
|
||||
const warehouseChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
setSelectedWarehouse(val as OptionType);
|
||||
updateFilter('warehouse', val ? ((val as OptionType).value as string) : '');
|
||||
};
|
||||
|
||||
const movementColumns: ColumnDef<Movement>[] = [
|
||||
{
|
||||
header: '#',
|
||||
@@ -200,33 +165,7 @@ const MovementTable = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-12 justify-end gap-4'>
|
||||
<SelectInput
|
||||
label='Produk'
|
||||
options={productOptions}
|
||||
isLoading={isLoadingProductOptions}
|
||||
value={selectedProduct}
|
||||
onChange={productChangeHandler}
|
||||
onInputChange={setProductInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Gudang'
|
||||
options={warehouseOptions}
|
||||
isLoading={isLoadingWarehouseOptions}
|
||||
value={selectedWarehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
onInputChange={setWarehouseInputValue}
|
||||
isClearable
|
||||
className={{
|
||||
wrapper: 'col-span-12 sm:col-span-4',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className='flex justify-end gap-4'>
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
|
||||
@@ -171,7 +171,17 @@ export const MovementFormSchema: Yup.ObjectSchema<MovementFormSchemaType> =
|
||||
}).nullable(),
|
||||
destination_warehouse_id: Yup.number()
|
||||
.required('Gudang tujuan wajib diisi!')
|
||||
.typeError('Gudang tujuan wajib diisi!'),
|
||||
.typeError('Gudang tujuan wajib diisi!')
|
||||
.test(
|
||||
'different-warehouse',
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!',
|
||||
function (value) {
|
||||
const { source_warehouse_id } = this.parent;
|
||||
return (
|
||||
!value || !source_warehouse_id || value !== source_warehouse_id
|
||||
);
|
||||
}
|
||||
),
|
||||
products: Yup.array()
|
||||
.of(ProductObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 produk!')
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Icon } from '@iconify/react';
|
||||
import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
DeliverySchema,
|
||||
} from '@/components/pages/inventory/movement/form/MovementForm.schema';
|
||||
import { SupplierApi, WarehouseApi } from '@/services/api/master-data';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { ProductWarehouseApi } from '@/services/api/inventory';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { MovementApi } from '@/services/api/inventory';
|
||||
@@ -100,11 +102,14 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
isLoadingOptions: isLoadingWarehouses,
|
||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
// ===== SELECT INPUT DATA =====
|
||||
const {
|
||||
setInputValue: setSupplierSelectInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSuppliers,
|
||||
} = useSelect(SupplierApi.basePath, 'id', 'name', 'search');
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'BOP',
|
||||
});
|
||||
|
||||
const warehousesUrl = `${WarehouseApi.basePath}?${new URLSearchParams({ search: warehouseSelectInputValue }).toString()}`;
|
||||
const { data: warehouses } = useSWR(
|
||||
@@ -171,6 +176,22 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
enableReinitialize: true,
|
||||
onSubmit: async (values) => {
|
||||
setMovementFormErrorMessage('');
|
||||
if (values.source_warehouse_id === values.destination_warehouse_id) {
|
||||
const sourceWarehouseName =
|
||||
(values.source_warehouse as WarehouseOptionType)?.label ||
|
||||
'Gudang asal';
|
||||
const destinationWarehouseName =
|
||||
(values.destination_warehouse as WarehouseOptionType)?.label ||
|
||||
'gudang tujuan';
|
||||
|
||||
setMovementFormErrorMessage(
|
||||
`Tidak bisa submit form. ${sourceWarehouseName} tidak boleh sama dengan ${destinationWarehouseName}.`
|
||||
);
|
||||
toast.error(
|
||||
`Tidak bisa submit form. Gudang asal dan tujuan tidak boleh sama!`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const documents: File[] = [];
|
||||
const deliveriesPayload = values.deliveries.map((d) => {
|
||||
let documentIndex = 0;
|
||||
@@ -305,6 +326,10 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
};
|
||||
};
|
||||
|
||||
const handleTransferDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
formik.setFieldValue('transfer_date', e.target.value);
|
||||
};
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
// Product Handlers
|
||||
const addProduct = () => {
|
||||
@@ -745,6 +770,31 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
}
|
||||
}, [formik.values.source_warehouse_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
formik.values.source_warehouse_id &&
|
||||
formik.values.destination_warehouse_id &&
|
||||
formik.values.source_warehouse_id ===
|
||||
formik.values.destination_warehouse_id
|
||||
) {
|
||||
formik.setFieldError(
|
||||
'destination_warehouse_id',
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
formik.values.source_warehouse_id,
|
||||
formik.values.destination_warehouse_id,
|
||||
formik.errors.destination_warehouse_id,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
@@ -792,13 +842,12 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
errorMessage={formik.errors.transfer_reason}
|
||||
readOnly={type === 'detail'}
|
||||
/>
|
||||
<TextInput
|
||||
<DateInput
|
||||
required
|
||||
label='Tanggal Transfer'
|
||||
type='date'
|
||||
name='transfer_date'
|
||||
value={formik.values.transfer_date}
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleTransferDateChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
formik.touched.transfer_date &&
|
||||
@@ -825,13 +874,41 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
placeholder='Pilih gudang asal...'
|
||||
value={formik.values.source_warehouse}
|
||||
onChange={(val) => {
|
||||
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',
|
||||
(val as WarehouseOptionType)?.value
|
||||
newSourceWarehouseId
|
||||
);
|
||||
|
||||
if (
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
@@ -896,13 +973,39 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
placeholder='Pilih gudang tujuan...'
|
||||
value={formik.values.destination_warehouse}
|
||||
onChange={(val) => {
|
||||
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',
|
||||
(val as WarehouseOptionType)?.value
|
||||
newDestinationWarehouseId
|
||||
);
|
||||
|
||||
if (
|
||||
formik.errors.destination_warehouse_id ===
|
||||
'Gudang tujuan tidak boleh sama dengan gudang asal!'
|
||||
) {
|
||||
formik.setFieldError('destination_warehouse_id', undefined);
|
||||
}
|
||||
}}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
@@ -1622,7 +1725,11 @@ const MovementForm = ({ type = 'add', initialValues }: MovementFormProps) => {
|
||||
hasInvalidQty ||
|
||||
hasExceededStock ||
|
||||
!formik.isValid ||
|
||||
formik.isSubmitting
|
||||
formik.isSubmitting ||
|
||||
(formik.values.source_warehouse_id ===
|
||||
formik.values.destination_warehouse_id &&
|
||||
formik.values.source_warehouse_id !== 0 &&
|
||||
formik.values.destination_warehouse_id !== 0)
|
||||
}
|
||||
>
|
||||
Submit
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import { OptionType } from '@/components/input/SelectInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
@@ -183,14 +183,18 @@ const MarketingTable = () => {
|
||||
);
|
||||
|
||||
const hasApprovable = selectedRowsData.some(
|
||||
(row) => row.latest_approval.step_number === 1
|
||||
(row) =>
|
||||
row.latest_approval.step_number === 1 &&
|
||||
row.latest_approval.action !== 'REJECTED'
|
||||
);
|
||||
const hasRejectable = selectedRowsData.some(
|
||||
(row) => row.latest_approval.step_number === 2
|
||||
(row) =>
|
||||
row.latest_approval.step_number === 1 &&
|
||||
row.latest_approval.action !== 'REJECTED'
|
||||
);
|
||||
|
||||
const disableApprove = !hasApprovable || hasRejectable;
|
||||
// const disableReject = !hasRejectable || hasApprovable;
|
||||
const disableApprove = !hasApprovable;
|
||||
const disableReject = !hasRejectable;
|
||||
|
||||
const idsToProcess =
|
||||
approveAction === 'APPROVED'
|
||||
@@ -204,15 +208,9 @@ const MarketingTable = () => {
|
||||
const approveMarketingHandler = async (notes: string) => {
|
||||
let idsToProcess: number[] = [];
|
||||
|
||||
if (approveAction === 'APPROVED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id);
|
||||
} else if (approveAction === 'REJECTED') {
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 2)
|
||||
.map((row) => row.id);
|
||||
}
|
||||
idsToProcess = selectedRowsData
|
||||
.filter((row) => row.latest_approval.step_number === 1)
|
||||
.map((row) => row.id);
|
||||
|
||||
if (idsToProcess.length === 0) {
|
||||
toast.error(`Tidak ada data yang valid untuk di ${approveAction}.`);
|
||||
@@ -263,8 +261,8 @@ const MarketingTable = () => {
|
||||
});
|
||||
|
||||
const getRowCanSelect = (row: Row<Marketing>): boolean => {
|
||||
const step = row.original.latest_approval?.step_number;
|
||||
return step === 1;
|
||||
const approval = row.original.latest_approval;
|
||||
return approval?.step_number === 1 && approval?.action !== 'REJECTED';
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -282,11 +280,6 @@ const MarketingTable = () => {
|
||||
placeholder: 'Cari Sales Order',
|
||||
}}
|
||||
/>
|
||||
<TableRowSizeSelector
|
||||
value={pageSize}
|
||||
onChange={pageSizeChangeHandler}
|
||||
options={ROWS_OPTIONS}
|
||||
/>
|
||||
<div className='flex flex-row gap-2'>
|
||||
<Button
|
||||
color='success'
|
||||
@@ -298,7 +291,7 @@ const MarketingTable = () => {
|
||||
Approve
|
||||
</Button>
|
||||
|
||||
{/* <Button
|
||||
<Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
className='justify-start text-sm'
|
||||
@@ -306,8 +299,36 @@ const MarketingTable = () => {
|
||||
>
|
||||
<Icon icon='material-symbols:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
<TableRowSizeSelector
|
||||
value={pageSize}
|
||||
onChange={pageSizeChangeHandler}
|
||||
options={ROWS_OPTIONS}
|
||||
>
|
||||
{/* select multiple product */}
|
||||
<SelectInput
|
||||
label='Product'
|
||||
isClearable
|
||||
placeholder='Pilih product'
|
||||
options={[]}
|
||||
isMulti
|
||||
/>
|
||||
{/* select status */}
|
||||
<SelectInput
|
||||
label='Status'
|
||||
isClearable
|
||||
placeholder='Pilih status'
|
||||
options={[]}
|
||||
/>
|
||||
{/* select customer */}
|
||||
<SelectInput
|
||||
label='Customer'
|
||||
isClearable
|
||||
placeholder='Pilih customer'
|
||||
options={[]}
|
||||
/>
|
||||
</TableRowSizeSelector>
|
||||
</div>
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
|
||||
@@ -32,7 +32,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
|
||||
import DeliveryOrderExport from '../pdf/DeliveryOrderExport';
|
||||
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
|
||||
|
||||
const MarketingDetail = ({
|
||||
initialValues,
|
||||
@@ -71,10 +71,10 @@ const MarketingDetail = ({
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
// const rejectClickHandler = () => {
|
||||
// setApprovalAction('REJECTED');
|
||||
// confirmationModal.openModal();
|
||||
// };
|
||||
const rejectClickHandler = () => {
|
||||
setApprovalAction('REJECTED');
|
||||
confirmationModal.openModal();
|
||||
};
|
||||
|
||||
const deliveryClickHandler = () => {
|
||||
deliveryModal.openModal();
|
||||
@@ -87,10 +87,11 @@ const MarketingDetail = ({
|
||||
const confirmationModalDeleteClickHandler = async () => {
|
||||
setIsLoading(true);
|
||||
const res = await MarketingApi.delete(initialValues?.id as number);
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing');
|
||||
toast.success(res?.message as string);
|
||||
refresh?.();
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const confirmationModalApproveClickHandler = async (notes: string) => {
|
||||
@@ -131,32 +132,45 @@ const MarketingDetail = ({
|
||||
<ApprovalSteps approvals={approvals} />
|
||||
)}
|
||||
<div className='flex-row flex gap-3'>
|
||||
{initialValues?.latest_approval?.step_number != 3 && (
|
||||
{initialValues?.latest_approval?.step_number == 1 && (
|
||||
<>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={approveClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 1}
|
||||
disabled={
|
||||
initialValues?.latest_approval?.step_number == 1 &&
|
||||
initialValues?.latest_approval?.action == 'REJECTED'
|
||||
}
|
||||
>
|
||||
<Icon icon='mdi:check' width={24} height={24} />
|
||||
Approve
|
||||
</Button>
|
||||
{/* <Button
|
||||
<Button
|
||||
color='error'
|
||||
onClick={rejectClickHandler}
|
||||
disabled={initialValues?.latest_approval?.step_number != 2}
|
||||
disabled={
|
||||
initialValues?.latest_approval?.step_number == 1 &&
|
||||
initialValues?.latest_approval?.action == 'REJECTED'
|
||||
}
|
||||
>
|
||||
<Icon icon='mdi:close' width={24} height={24} />
|
||||
Reject
|
||||
</Button> */}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{initialValues?.latest_approval?.step_number == 2 && (
|
||||
{initialValues?.latest_approval?.step_number != 1 && (
|
||||
<Button
|
||||
color='success'
|
||||
href={`/marketing/add/delivery-orders?marketingId=${initialValues?.id}`}
|
||||
href={
|
||||
initialValues?.latest_approval?.step_number == 3
|
||||
? `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
: `/marketing/add/delivery-orders?marketingId=${initialValues?.id}`
|
||||
}
|
||||
>
|
||||
<Icon icon='mdi:truck' width={24} height={24} />
|
||||
{initialValues?.latest_approval?.step_number == 3
|
||||
? 'Edit '
|
||||
: 'Tambah '}
|
||||
Delivery Order
|
||||
</Button>
|
||||
)}
|
||||
@@ -398,14 +412,16 @@ const MarketingDetail = ({
|
||||
</Card>
|
||||
)}
|
||||
<div className='flex flex-row gap-3'>
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
{initialValues?.latest_approval?.step_number != 3 && (
|
||||
<Button
|
||||
color='warning'
|
||||
type='button'
|
||||
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
|
||||
>
|
||||
<Icon icon='mdi:pencil' width={24} height={24} />
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
<Button color='error' onClick={deleteClickHandler}>
|
||||
<Icon icon='mdi:delete' width={24} height={24} />
|
||||
Hapus
|
||||
@@ -429,7 +445,7 @@ const MarketingDetail = ({
|
||||
<ConfirmationModalWithNotes
|
||||
ref={confirmationModal.ref}
|
||||
type={approvalAction === 'APPROVED' ? 'success' : 'error'}
|
||||
text={`Apakah anda yakin ingin ${approvalAction} data penjualan ini?`}
|
||||
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data penjualan ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
|
||||
@@ -49,22 +49,23 @@ export const SalesOrderSchema: Yup.ObjectSchema<SalesOrderSchemaType> =
|
||||
export const DeliveryOrderSchema: Yup.ObjectSchema<DeliveryOrderSchemaType> =
|
||||
Yup.object({
|
||||
delivery_order: Yup.array()
|
||||
.of(DeliveryOrderProductSchema)
|
||||
.min(1, 'Pengiriman wajib diisi!')
|
||||
.required()
|
||||
.required('Pengiriman wajib diisi!')
|
||||
.test(
|
||||
'at-least-one-delivery-date',
|
||||
'Minimal ada satu tanggal pengiriman yang harus diisi!',
|
||||
(value) => {
|
||||
if (!value || value.length == 0) {
|
||||
return false;
|
||||
}
|
||||
return value.some(
|
||||
(item) =>
|
||||
item.delivery_date !== null &&
|
||||
item.delivery_date !== undefined &&
|
||||
item.delivery_date !== ''
|
||||
);
|
||||
'at-least-one-valid-row',
|
||||
'Minimal ada satu baris pengiriman yang valid!',
|
||||
function (items) {
|
||||
if (!items || items.length === 0) return false;
|
||||
|
||||
// VALIDASI: minimal 1 item valid full
|
||||
const itemSchema = DeliveryOrderProductSchema;
|
||||
|
||||
const hasValidItem = items.some((item) => {
|
||||
if (!item) return false;
|
||||
return itemSchema.isValidSync(item, { abortEarly: true });
|
||||
});
|
||||
|
||||
return hasValidItem;
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
@@ -47,13 +47,23 @@ import SalesOrderProductForm from './repeater/sales-order/SalesOrderProductForm'
|
||||
import DeliveryOrderProductTable from './table-view/DeliveryOrderProductTable';
|
||||
import DeliveryOrderProductForm from './repeater/delivery-order/DeliverOrderProduct';
|
||||
import { DeliveryOrderProductFormValues } from './repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import DebouncedTextArea from '@/components/input/DebouncedTextArea';
|
||||
|
||||
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
|
||||
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
|
||||
const MemoizedDeliveryOrderProductTable = memo(DeliveryOrderProductTable);
|
||||
const MemoizedDeliveryOrderProductForm = memo(DeliveryOrderProductForm);
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
// ================== EXTERNAL HELPER FUNCTION ==================
|
||||
export interface ProductCalculationFields {
|
||||
qty: string | number | undefined;
|
||||
unit_price: string | number | undefined;
|
||||
total_price: string | number | undefined;
|
||||
avg_weight: string | number | undefined;
|
||||
total_weight: string | number | undefined;
|
||||
}
|
||||
|
||||
export const SalesProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
@@ -76,8 +86,7 @@ const MarketingProductToFieldValues = (
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const DeliveryProductToFieldValues = (
|
||||
export const DeliveryProductToFieldValues = (
|
||||
salesOrders: BaseSalesOrder[],
|
||||
delivery: BaseDeliveryOrder
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
@@ -119,8 +128,7 @@ const DeliveryProductToFieldValues = (
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
const mergeSOwithDO = (
|
||||
export const mergeSOwithDO = (
|
||||
salesOrders: SalesOrderProductFormValues[],
|
||||
deliveryOrders: DeliveryOrderProductFormValues[]
|
||||
): DeliveryOrderProductFormValues[] => {
|
||||
@@ -135,15 +143,63 @@ const mergeSOwithDO = (
|
||||
delivery_date: delivery?.delivery_date || undefined,
|
||||
do_number: delivery?.do_number || undefined,
|
||||
vehicle_number: delivery?.vehicle_number || so.vehicle_number,
|
||||
unit_price: delivery?.unit_price ?? so.unit_price,
|
||||
total_weight: delivery?.total_weight ?? so.total_weight,
|
||||
qty: delivery?.qty ?? so.qty,
|
||||
avg_weight: delivery?.avg_weight ?? so.avg_weight,
|
||||
total_price: delivery?.total_price ?? so.total_price,
|
||||
unit_price: delivery?.unit_price,
|
||||
total_weight: delivery?.total_weight,
|
||||
qty: delivery?.qty,
|
||||
avg_weight: delivery?.avg_weight,
|
||||
total_price: delivery?.total_price,
|
||||
marketing_product: so, // jika ada, override
|
||||
} as DeliveryOrderProductFormValues;
|
||||
});
|
||||
};
|
||||
export const recalculate = (
|
||||
field: string,
|
||||
values: ProductCalculationFields
|
||||
) => {
|
||||
console.log('Values');
|
||||
console.log(values);
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } = values;
|
||||
const result: Partial<ProductCalculationFields> = {};
|
||||
if (field == 'unit_price' || field == 'total_price' || field == 'qty') {
|
||||
if (qty && unit_price && (field == 'unit_price' || field == 'qty')) {
|
||||
result.total_price = Number(qty) * Number(unit_price);
|
||||
} else if (qty && total_price && field == 'total_price') {
|
||||
result.unit_price = Number(total_price) / Number(qty);
|
||||
}
|
||||
}
|
||||
if (field == 'avg_weight' || field == 'total_weight' || field == 'qty') {
|
||||
if (qty && avg_weight && (field == 'avg_weight' || field == 'qty')) {
|
||||
result.total_weight = Number(qty) * Number(avg_weight);
|
||||
} else if (qty && total_weight && field == 'total_weight') {
|
||||
result.avg_weight = Number(total_weight) / Number(qty);
|
||||
}
|
||||
}
|
||||
console.log('Result');
|
||||
console.log(result);
|
||||
return result;
|
||||
};
|
||||
export const getSubmitField = (values: ProductCalculationFields) => {
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } = values;
|
||||
|
||||
// Harga logic
|
||||
if (qty && unit_price && !total_price) {
|
||||
return 'unit_price';
|
||||
}
|
||||
if (qty && total_price && !unit_price) {
|
||||
return 'total_price';
|
||||
}
|
||||
|
||||
// Bobot logic
|
||||
if (qty && avg_weight && !total_weight) {
|
||||
return 'avg_weight';
|
||||
}
|
||||
if (qty && total_weight && !avg_weight) {
|
||||
return 'total_weight';
|
||||
}
|
||||
|
||||
// Tidak ada yang perlu dihitung
|
||||
return '';
|
||||
};
|
||||
|
||||
const MarketingForm = ({
|
||||
formType = 'add',
|
||||
@@ -162,19 +218,21 @@ const MarketingForm = ({
|
||||
useState<SalesOrderProductFormValues | null>(null);
|
||||
const [selectedDeliveryProduct, setSelectedDeliveryProduct] =
|
||||
useState<DeliveryOrderProductFormValues | null>(null);
|
||||
|
||||
const [deliveryFormState, setDeliveryFormState] = useState<'add' | 'edit'>(
|
||||
'add'
|
||||
);
|
||||
const [deliveryOrderValues, setDeliveryOrderValues] = useState<
|
||||
DeliveryOrderProductFormValues[]
|
||||
>(
|
||||
mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
)
|
||||
);
|
||||
|
||||
// Repeater Props
|
||||
// ================== REPEATER ==================
|
||||
const addSOModal = useModal();
|
||||
const addDOModal = useModal();
|
||||
const [rowSOSelection, setRowSOSelection] = useState<Record<string, boolean>>(
|
||||
@@ -183,19 +241,14 @@ const MarketingForm = ({
|
||||
const selectedRowSOIds = Object.keys(rowSOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
const [rowDOSelection, setRowDOSelection] = useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const selectedRowDOIds = Object.keys(rowDOSelection).map((item) =>
|
||||
parseInt(item)
|
||||
);
|
||||
|
||||
// End Repeater Props
|
||||
// ================== FETCH OPTIONS ==================
|
||||
const {
|
||||
options: customerOptions,
|
||||
isLoadingOptions: isLoadingCustomerOptions,
|
||||
} = useSelect<Customer>(CustomerApi.basePath, 'id', 'name');
|
||||
|
||||
// ================== SETUP FORMIK ==================
|
||||
const formikInitialValues = useMemo<
|
||||
SalesOrderFormValues & DeliveryOrderFormValues
|
||||
>(() => {
|
||||
@@ -212,17 +265,16 @@ const MarketingForm = ({
|
||||
: null,
|
||||
sales_order:
|
||||
initialValues?.sales_order?.map((product) =>
|
||||
MarketingProductToFieldValues(product)
|
||||
SalesProductToFieldValues(product)
|
||||
) ?? [],
|
||||
delivery_order: mergeSOwithDO(
|
||||
initialValues?.sales_order?.map(MarketingProductToFieldValues) ?? [],
|
||||
initialValues?.sales_order?.map(SalesProductToFieldValues) ?? [],
|
||||
initialValues?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(initialValues.sales_order, delivery)
|
||||
) ?? []
|
||||
),
|
||||
};
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik<SalesOrderFormValues & DeliveryOrderFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: formikInitialValues,
|
||||
@@ -297,14 +349,7 @@ const MarketingForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return formik.values.sales_order.reduce(
|
||||
(total, product) =>
|
||||
total + parseFloat((product.total_price as string) || '0'),
|
||||
0
|
||||
);
|
||||
}, [formik.values.sales_order]);
|
||||
|
||||
// ================== FORM REPEATER HANDLER ==================
|
||||
const createMarketingHandler = async (values: CreateSalesOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(values);
|
||||
@@ -334,7 +379,6 @@ const MarketingForm = ({
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const createDeliveryHandler = async (values: CreateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
@@ -350,9 +394,7 @@ const MarketingForm = ({
|
||||
)
|
||||
) ?? []
|
||||
);
|
||||
router.push(
|
||||
`/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
|
||||
);
|
||||
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
}
|
||||
if (isResponseError(createDeliveryRes)) {
|
||||
console.log(createDeliveryRes);
|
||||
@@ -360,7 +402,6 @@ const MarketingForm = ({
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const updateDeliveryHandler = async (values: UpdateDeliveryOrderPayload) => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
@@ -371,15 +412,18 @@ const MarketingForm = ({
|
||||
if (isResponseSuccess(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
toast.success(updateDeliveryRes?.message as string);
|
||||
// router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
setDeliveryOrderValues(
|
||||
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
updateDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
mergeSOwithDO(
|
||||
formik.values.sales_order,
|
||||
updateDeliveryRes.data?.delivery_order?.flatMap((delivery) =>
|
||||
DeliveryProductToFieldValues(
|
||||
updateDeliveryRes.data?.sales_order,
|
||||
delivery
|
||||
)
|
||||
) ?? []
|
||||
)
|
||||
);
|
||||
router.push(`/marketing/detail?marketingId=${initialValues?.id}`);
|
||||
}
|
||||
if (isResponseError(updateDeliveryRes)) {
|
||||
console.log(updateDeliveryRes);
|
||||
@@ -388,6 +432,7 @@ const MarketingForm = ({
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// ================== MARKETING HANDLER ==================
|
||||
const deleteMarketingHandler = async () => {
|
||||
setIsLoading(true);
|
||||
console.log(initialValues?.id);
|
||||
@@ -404,28 +449,27 @@ const MarketingForm = ({
|
||||
}
|
||||
setIsLoading(false);
|
||||
deleteModal.closeModal();
|
||||
router.push('/marketing/sales-orders');
|
||||
router.push('/marketing');
|
||||
};
|
||||
|
||||
const handleChangeCustomer = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('customer_id', (val as OptionType)?.value);
|
||||
formik.setFieldValue('customer', val as OptionType);
|
||||
},
|
||||
[formik]
|
||||
[]
|
||||
);
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteModal.openModal();
|
||||
}, [deleteModal]);
|
||||
|
||||
// Repeater Handle
|
||||
const handleDeleteSO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter((p) => p.id != id)
|
||||
);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
// ================== SALES ORDER HANDLER ==================
|
||||
const handleDeleteSO = useCallback((id: number) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
'sales_order',
|
||||
currentProducts.filter((p) => p.id != id)
|
||||
);
|
||||
}, []);
|
||||
const handleBulkDeleteSO = useCallback(() => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
formik.setFieldValue(
|
||||
@@ -435,10 +479,7 @@ const MarketingForm = ({
|
||||
)
|
||||
);
|
||||
setRowSOSelection({});
|
||||
}, [formik, selectedRowSOIds]);
|
||||
const handleDelete = useCallback(() => {
|
||||
deleteModal.openModal();
|
||||
}, [deleteModal]);
|
||||
}, [selectedRowSOIds]);
|
||||
const handleAddSOClick = useCallback(() => {
|
||||
setSelectedMarketingProduct(null);
|
||||
addSOModal.openModal();
|
||||
@@ -446,48 +487,54 @@ const MarketingForm = ({
|
||||
const handleAddSubmitSO = useCallback(
|
||||
async (values: SalesOrderProductFormValues) => {
|
||||
const currentProducts = formik.values.sales_order;
|
||||
|
||||
const newValues = {
|
||||
...values,
|
||||
id: values.id ?? Date.now(),
|
||||
};
|
||||
|
||||
formik.setFieldValue('sales_order', [...currentProducts, newValues]);
|
||||
const existingIndex = currentProducts.findIndex(
|
||||
(item) =>
|
||||
item.kandang_id === newValues.kandang_id &&
|
||||
item.product_warehouse_id === newValues.product_warehouse_id
|
||||
);
|
||||
|
||||
let updatedProducts = [];
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Overwrite
|
||||
updatedProducts = currentProducts.map((item, index) =>
|
||||
index === existingIndex ? newValues : item
|
||||
);
|
||||
} else {
|
||||
// Add new item
|
||||
updatedProducts = [...currentProducts, newValues];
|
||||
}
|
||||
|
||||
formik.setFieldValue('sales_order', updatedProducts);
|
||||
|
||||
addSOModal.closeModal();
|
||||
},
|
||||
[formik, addSOModal]
|
||||
[addSOModal]
|
||||
);
|
||||
|
||||
const handleDeleteDO = useCallback(
|
||||
(id: number) => {
|
||||
const currentProducts = formik.values.delivery_order;
|
||||
setDeliveryOrderValues((prev) => prev.filter((p) => p.id !== id));
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
// ================== DELIVERY ORDER HANDLER ==================
|
||||
const handleEditDO = useCallback(
|
||||
(id: number) => {
|
||||
(id: number, values?: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryFormState('edit');
|
||||
const currentProducts = formik.values.delivery_order.find(
|
||||
(product) => product.id == id
|
||||
);
|
||||
setSelectedDeliveryProduct(currentProducts ?? null);
|
||||
setSelectedDeliveryProduct(values ?? currentProducts ?? null);
|
||||
addDOModal.openModal();
|
||||
},
|
||||
[formik]
|
||||
[addDOModal]
|
||||
);
|
||||
const handleBulkDeleteDO = useCallback(() => {
|
||||
setDeliveryOrderValues((prev) =>
|
||||
prev.filter((product) => !selectedRowDOIds.includes(product.id ?? -1))
|
||||
);
|
||||
|
||||
setRowDOSelection({});
|
||||
}, [formik, selectedRowDOIds]);
|
||||
|
||||
const handleAddDOClick = useCallback(() => {
|
||||
setDeliveryFormState('add');
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.openModal();
|
||||
}, [addDOModal]);
|
||||
|
||||
const handleAddSubmitDO = useCallback(
|
||||
async (values: DeliveryOrderProductFormValues) => {
|
||||
const newValues = {
|
||||
@@ -496,23 +543,10 @@ const MarketingForm = ({
|
||||
};
|
||||
|
||||
setDeliveryOrderValues((prev) => [...prev, newValues]);
|
||||
|
||||
addDOModal.closeModal();
|
||||
setSelectedDeliveryProduct(null);
|
||||
},
|
||||
[formik, addDOModal]
|
||||
);
|
||||
const handleInputDate = useCallback(
|
||||
(newData: DeliveryOrderProductFormValues) => {
|
||||
setDeliveryOrderValues((prev) => {
|
||||
return prev.map((item) => {
|
||||
if (item.marketing_product_id == newData.marketing_product_id) {
|
||||
return newData;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
});
|
||||
},
|
||||
[]
|
||||
[addDOModal]
|
||||
);
|
||||
const handleUpdateDO = useCallback(
|
||||
async (id: number, values: DeliveryOrderProductFormValues) => {
|
||||
@@ -521,12 +555,11 @@ const MarketingForm = ({
|
||||
product.id === id ? { ...product, ...values } : product
|
||||
)
|
||||
);
|
||||
setSelectedDeliveryProduct(null);
|
||||
addDOModal.closeModal();
|
||||
setSelectedDeliveryProduct(null);
|
||||
},
|
||||
[formik, addDOModal]
|
||||
[addDOModal]
|
||||
);
|
||||
// End Repeater Handle
|
||||
|
||||
const memoSalesOrder = formik.values.sales_order;
|
||||
|
||||
@@ -534,6 +567,14 @@ const MarketingForm = ({
|
||||
formik.setFieldValue('delivery_order', deliveryOrderValues);
|
||||
}, [deliveryOrderValues, initialValues]);
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return memoSalesOrder.reduce(
|
||||
(total, product) =>
|
||||
total + parseFloat((product.total_price as string) || '0'),
|
||||
0
|
||||
);
|
||||
}, [memoSalesOrder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
@@ -545,6 +586,7 @@ const MarketingForm = ({
|
||||
title={`${formType == 'add' || formType == 'add_deliver' ? 'Tambah' : 'Edit'} ${formType === 'add_deliver' || formType === 'edit_deliver' ? 'Delivery' : 'Sales'} Order`}
|
||||
backUrl='/marketing'
|
||||
/>
|
||||
{/* Input Cutomer And Date */}
|
||||
<Card
|
||||
title='Informasi Order'
|
||||
className={{
|
||||
@@ -580,28 +622,27 @@ const MarketingForm = ({
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
{(formType == 'add' || formType == 'edit') && (
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* <div className='text-blue-500'>{JSON.stringify(initialValues)}</div>
|
||||
<div className='text-green-500'>{JSON.stringify(formik.values)}</div>
|
||||
<div className='text-red-500'>{JSON.stringify(formik.errors)}</div> */}
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={formType}
|
||||
data={memoSalesOrder}
|
||||
rowSelection={rowSOSelection}
|
||||
setRowSelection={setRowSOSelection}
|
||||
selectedRowIds={selectedRowSOIds}
|
||||
onDelete={handleDeleteSO}
|
||||
onBulkDelete={handleBulkDeleteSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Input Table Repeater Sales Order */}
|
||||
<Card
|
||||
title='Informasi Produk'
|
||||
className={{
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
<MemoizedSalesOrderProductTable
|
||||
formType={formType}
|
||||
data={memoSalesOrder}
|
||||
rowSelection={rowSOSelection}
|
||||
setRowSelection={setRowSOSelection}
|
||||
selectedRowIds={selectedRowSOIds}
|
||||
onDelete={handleDeleteSO}
|
||||
onBulkDelete={handleBulkDeleteSO}
|
||||
onAddProductClick={handleAddSOClick}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Input Table Repeater Delivery Order */}
|
||||
{(formType == 'add_deliver' || formType == 'edit_deliver') &&
|
||||
initialValues?.sales_order &&
|
||||
initialValues?.sales_order.length > 0 && (
|
||||
@@ -611,29 +652,24 @@ const MarketingForm = ({
|
||||
wrapper: 'bg-white w-full',
|
||||
}}
|
||||
>
|
||||
{/* {JSON.stringify(memoSalesOrder)} */}
|
||||
{/* <small>{JSON.stringify(memoDeliveryOrder)}</small> */}
|
||||
{/* <small className='block text-error'>
|
||||
{/* <div className='text-blue-500'>
|
||||
{JSON.stringify(formik.values)}
|
||||
</div>
|
||||
<div className='text-red-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small> */}
|
||||
</div> */}
|
||||
<MemoizedDeliveryOrderProductTable
|
||||
formType={formType}
|
||||
data={deliveryOrderValues}
|
||||
salesOrder={memoSalesOrder}
|
||||
rowSelection={rowDOSelection}
|
||||
setRowSelection={setRowDOSelection}
|
||||
selectedRowIds={selectedRowDOIds}
|
||||
onDelete={handleDeleteDO}
|
||||
onEdit={handleEditDO}
|
||||
onBulkDelete={handleBulkDeleteDO}
|
||||
onAddProductClick={handleAddDOClick}
|
||||
onInputDate={handleInputDate}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Input Notes */}
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<TextArea
|
||||
<DebouncedTextArea
|
||||
required
|
||||
name='notes'
|
||||
label='Catatan'
|
||||
@@ -652,6 +688,8 @@ const MarketingForm = ({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form Actions */}
|
||||
<div className='flex flex-row items-start justify-center gap-2 mt-4'>
|
||||
<Button type='reset' color='warning' disabled={formik.isSubmitting}>
|
||||
Reset
|
||||
@@ -665,6 +703,8 @@ const MarketingForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Actions button */}
|
||||
{formType == 'edit' && (
|
||||
<div className='flex flex-row justify-start'>
|
||||
<Button
|
||||
@@ -678,6 +718,8 @@ const MarketingForm = ({
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modals */}
|
||||
<Modal
|
||||
ref={addSOModal.ref}
|
||||
closeOnBackdrop
|
||||
@@ -701,6 +743,7 @@ const MarketingForm = ({
|
||||
<MemoizedSalesOrderProductForm
|
||||
onSubmitForm={handleAddSubmitSO}
|
||||
initialValues={selectedMarketingProduct ?? undefined}
|
||||
exisitingValues={memoSalesOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -728,7 +771,9 @@ const MarketingForm = ({
|
||||
</div>
|
||||
<div>
|
||||
<MemoizedDeliveryOrderProductForm
|
||||
formState={deliveryFormState}
|
||||
salesOrders={initialValues?.sales_order ?? []}
|
||||
exisitingValues={deliveryOrderValues}
|
||||
onSubmitForm={handleAddSubmitDO}
|
||||
initialValues={selectedDeliveryProduct ?? undefined}
|
||||
onUpdateForm={handleUpdateDO}
|
||||
|
||||
+2
-18
@@ -1,9 +1,5 @@
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '../sales-order/SalesOrderProduct.schema';
|
||||
import { de } from 'react-day-picker/locale';
|
||||
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
|
||||
type DeliveryOrderProductSchemaType = {
|
||||
id?: number | undefined;
|
||||
@@ -42,22 +38,10 @@ export const DeliveryOrderProductSchema: Yup.ObjectSchema<DeliveryOrderProductSc
|
||||
.min(1, 'Total Penjualan wajib diisi!')
|
||||
.required('Total Penjualan wajib diisi!'),
|
||||
vehicle_number: Yup.string().required('Nomor Kendaraan wajib diisi!'),
|
||||
delivery_date: Yup.string()
|
||||
.required('Tanggal Pengiriman wajib diisi!')
|
||||
.nullable()
|
||||
.optional(),
|
||||
delivery_date: Yup.string().required('Tanggal Pengiriman wajib diisi!'),
|
||||
do_number: Yup.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export type DeliveryOrderProductFormValues = Yup.InferType<
|
||||
typeof DeliveryOrderProductSchema
|
||||
>;
|
||||
|
||||
// "marketing_product_id": 3,
|
||||
// "qty": 20,
|
||||
// "unit_price": 1000,
|
||||
// "avg_weight": 1.1,
|
||||
// "total_weight": 220,
|
||||
// "total_price": 20000,
|
||||
// "delivery_date": "2025-11-09",
|
||||
// "vehicle_number": "D 4321 XXX"
|
||||
|
||||
+99
-59
@@ -10,20 +10,24 @@ import NumberInput from '@/components/input/NumberInput';
|
||||
import PatternInput from '@/components/input/PatternInput';
|
||||
import { formatVechicleNumber } from '@/lib/helper';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import { SalesOrderProductFormValues } from '../sales-order/SalesOrderProduct.schema';
|
||||
import { BaseSalesOrder } from '@/types/api/marketing/marketing';
|
||||
import Badge from '@/components/Badge';
|
||||
import { SalesProductToFieldValues } from '@/components/pages/marketing/form/MarketingForm';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const DeliveryOrderProductForm = ({
|
||||
formState,
|
||||
salesOrders,
|
||||
initialValues,
|
||||
exisitingValues,
|
||||
onSubmitForm,
|
||||
onUpdateForm,
|
||||
}: {
|
||||
formState: 'add' | 'edit';
|
||||
salesOrders: BaseSalesOrder[];
|
||||
initialValues?: DeliveryOrderProductFormValues;
|
||||
exisitingValues?: DeliveryOrderProductFormValues[];
|
||||
onSubmitForm?: (value: DeliveryOrderProductFormValues) => Promise<void>;
|
||||
onUpdateForm?: (
|
||||
id: number,
|
||||
@@ -34,13 +38,18 @@ const DeliveryOrderProductForm = ({
|
||||
const [selectedProduct, setSelectedProduct] = useState<OptionType | null>(
|
||||
null
|
||||
);
|
||||
const [currentInput, setCurrentInput] = useState<string>('');
|
||||
const salesOrder = salesOrders.find(
|
||||
(item) => item.id === initialValues?.marketing_product_id
|
||||
);
|
||||
|
||||
const formik = useFormik<DeliveryOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
delivery_date: initialValues?.delivery_date || undefined,
|
||||
vehicle_number: initialValues?.vehicle_number || undefined,
|
||||
marketing_product_id: initialValues?.marketing_product_id || undefined,
|
||||
marketing_product_id:
|
||||
salesOrder?.id || initialValues?.marketing_product_id || undefined,
|
||||
unit_price: initialValues?.unit_price || undefined,
|
||||
total_weight: initialValues?.total_weight || undefined,
|
||||
qty: initialValues?.qty || undefined,
|
||||
@@ -48,15 +57,33 @@ const DeliveryOrderProductForm = ({
|
||||
total_price: initialValues?.total_price || undefined,
|
||||
marketing_product: initialValues?.marketing_product || undefined,
|
||||
},
|
||||
validationSchema: DeliveryOrderProductSchema,
|
||||
isInitialValid: false,
|
||||
validationSchema: Yup.object().shape({
|
||||
...DeliveryOrderProductSchema.fields,
|
||||
|
||||
qty: Yup.lazy((_, context) => {
|
||||
// values diambil aman dari context
|
||||
const { parent } = context;
|
||||
|
||||
const mpId = parent?.marketing_product_id;
|
||||
const selectedSO = salesOrders.find((item) => item.id === mpId);
|
||||
|
||||
const maxQty = selectedSO?.qty ?? Infinity;
|
||||
|
||||
return Yup.number()
|
||||
.min(1, 'Kuantitas wajib diisi!')
|
||||
.max(maxQty, `Maksimal kuantitas adalah ${maxQty}`)
|
||||
.required('Kuantitas wajib diisi!');
|
||||
}),
|
||||
}),
|
||||
validateOnChange: true,
|
||||
validateOnBlur: true,
|
||||
validateOnChange: false,
|
||||
onSubmit: async (values) => {
|
||||
setFormErrorMessage('');
|
||||
if (initialValues?.id) {
|
||||
await onUpdateForm?.(initialValues.id, values);
|
||||
} else {
|
||||
await onSubmitForm?.(values);
|
||||
await onUpdateForm?.(values.marketing_product_id as number, values);
|
||||
}
|
||||
handleResetForm();
|
||||
},
|
||||
@@ -81,6 +108,7 @@ const DeliveryOrderProductForm = ({
|
||||
};
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
setCurrentInput(field);
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
||||
formik.values;
|
||||
|
||||
@@ -101,47 +129,37 @@ const DeliveryOrderProductForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const MarketingProductToFieldValues = (
|
||||
product: BaseSalesOrder
|
||||
): SalesOrderProductFormValues => {
|
||||
return {
|
||||
id: product.id,
|
||||
vehicle_number: product.vehicle_number,
|
||||
kandang_id: product.product_warehouse.warehouse.id,
|
||||
kandang: {
|
||||
value: product.product_warehouse.warehouse.id,
|
||||
label: product.product_warehouse.warehouse.name,
|
||||
},
|
||||
product_warehouse: {
|
||||
value: product.product_warehouse.id,
|
||||
label: product.product_warehouse.product.name,
|
||||
},
|
||||
product_warehouse_id: product.product_warehouse.id,
|
||||
unit_price: product.unit_price,
|
||||
total_weight: product.total_weight,
|
||||
qty: product.qty,
|
||||
avg_weight: product.avg_weight,
|
||||
total_price: product.total_price,
|
||||
};
|
||||
};
|
||||
|
||||
const options = salesOrders.map((item) => ({
|
||||
value: item.id,
|
||||
label: `${item.product_warehouse.product.name} - ${item.product_warehouse.warehouse.name}`,
|
||||
}));
|
||||
const options = exisitingValues
|
||||
?.map((item) => {
|
||||
if (!Boolean(item.qty)) {
|
||||
return {
|
||||
value: item.id,
|
||||
label: `${item.marketing_product?.product_warehouse?.label} - ${item.marketing_product?.kandang?.label}`,
|
||||
} as OptionType;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
?.filter((item) => item != null) as OptionType[];
|
||||
|
||||
const { setValues: setFormikValues } = formik;
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setFormikValues(initialValues);
|
||||
const value = salesOrders.find(
|
||||
(item) => item.id === initialValues.marketing_product_id
|
||||
);
|
||||
setSelectedProduct({
|
||||
value: value?.id,
|
||||
label: `${value?.product_warehouse.product.name} - ${value?.product_warehouse.warehouse.name}`,
|
||||
} as OptionType);
|
||||
if (!Boolean(initialValues.qty)) {
|
||||
handleResetForm();
|
||||
} else {
|
||||
setFormikValues(initialValues);
|
||||
// const value = exisitingValues?.find(
|
||||
// (item) => item.id === initialValues?.id
|
||||
// );
|
||||
if (initialValues?.marketing_product_id) {
|
||||
setSelectedProduct({
|
||||
value: initialValues?.id,
|
||||
label: `${initialValues?.marketing_product?.product_warehouse?.label} - ${initialValues?.marketing_product?.kandang?.label}`,
|
||||
} as OptionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
@@ -149,17 +167,21 @@ const DeliveryOrderProductForm = ({
|
||||
<>
|
||||
<form
|
||||
className='size-full'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleBlurField(currentInput);
|
||||
formik.handleSubmit(e);
|
||||
}}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{/* <small className='block text-blue-500'>
|
||||
{JSON.stringify(initialValues)}
|
||||
</small>
|
||||
<small className='block text-red-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
{JSON.stringify(exisitingValues)}
|
||||
</small>
|
||||
<small className='block text-emerald-500'>
|
||||
{JSON.stringify(formik.values)}
|
||||
</small> */}
|
||||
{/* <small className='block text-red-500'>
|
||||
{JSON.stringify(formik.errors)}
|
||||
</small>
|
||||
<div className='hidden'>
|
||||
{JSON.stringify(formik.values.marketing_product)}
|
||||
@@ -176,14 +198,14 @@ const DeliveryOrderProductForm = ({
|
||||
options={options}
|
||||
label='Produk'
|
||||
placeholder='Pilih Produk'
|
||||
isDisabled
|
||||
isDisabled={formState == 'edit'}
|
||||
value={
|
||||
selectedProduct
|
||||
? ({
|
||||
value: selectedProduct?.value,
|
||||
label: salesOrders.find(
|
||||
label: exisitingValues?.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse.product.name,
|
||||
)?.marketing_product?.product_warehouse?.label,
|
||||
} as OptionType)
|
||||
: null
|
||||
}
|
||||
@@ -191,7 +213,7 @@ const DeliveryOrderProductForm = ({
|
||||
const selected = value as OptionType;
|
||||
setSelectedProduct(selected);
|
||||
|
||||
const so = salesOrders.find(
|
||||
const so = salesOrders?.find(
|
||||
(item) => item.id === selected?.value
|
||||
);
|
||||
if (!so) {
|
||||
@@ -212,7 +234,7 @@ const DeliveryOrderProductForm = ({
|
||||
formik.setValues({
|
||||
...formik.values,
|
||||
marketing_product_id: selected.value as number,
|
||||
marketing_product: MarketingProductToFieldValues(so),
|
||||
marketing_product: SalesProductToFieldValues(so),
|
||||
qty: formik.values.qty || so.qty,
|
||||
unit_price: so.unit_price,
|
||||
total_price: so.total_price,
|
||||
@@ -230,9 +252,9 @@ const DeliveryOrderProductForm = ({
|
||||
className={{ badge: 'whitespace-nowrap font-semibold' }}
|
||||
>
|
||||
{
|
||||
salesOrders.find(
|
||||
exisitingValues?.find(
|
||||
(item) => item.id === selectedProduct?.value
|
||||
)?.product_warehouse?.warehouse?.name
|
||||
)?.marketing_product?.kandang?.label
|
||||
}
|
||||
</Badge>
|
||||
)
|
||||
@@ -254,6 +276,9 @@ const DeliveryOrderProductForm = ({
|
||||
}
|
||||
errorMessage={formik.errors.delivery_date}
|
||||
placeholder='Pilih Tanggal'
|
||||
className={{
|
||||
inputWrapper: 'bg-white',
|
||||
}}
|
||||
required
|
||||
/>
|
||||
|
||||
@@ -278,7 +303,10 @@ const DeliveryOrderProductForm = ({
|
||||
label='Kuantitas'
|
||||
name='qty'
|
||||
value={formik.values.qty}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('qty')}
|
||||
isError={Boolean(formik.errors.qty)}
|
||||
errorMessage={formik.errors.qty}
|
||||
@@ -290,7 +318,10 @@ const DeliveryOrderProductForm = ({
|
||||
label='Avg. Bobot (Kg)'
|
||||
name='avg_weight'
|
||||
value={formik.values.avg_weight}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('avg_weight')}
|
||||
isError={Boolean(formik.errors.avg_weight)}
|
||||
errorMessage={formik.errors.avg_weight}
|
||||
@@ -302,7 +333,10 @@ const DeliveryOrderProductForm = ({
|
||||
label='Harga Satuan (Rp)'
|
||||
name='unit_price'
|
||||
value={formik.values.unit_price}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('unit_price')}
|
||||
isError={Boolean(formik.errors.unit_price)}
|
||||
errorMessage={formik.errors.unit_price}
|
||||
@@ -314,7 +348,10 @@ const DeliveryOrderProductForm = ({
|
||||
label='Total Bobot (Kg)'
|
||||
name='total_weight'
|
||||
value={formik.values.total_weight}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('total_weight')}
|
||||
isError={Boolean(formik.errors.total_weight)}
|
||||
errorMessage={formik.errors.total_weight}
|
||||
@@ -326,7 +363,10 @@ const DeliveryOrderProductForm = ({
|
||||
label='Total Penjualan (Rp)'
|
||||
name='total_price'
|
||||
value={formik.values.total_price}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('total_price')}
|
||||
isError={Boolean(formik.errors.total_price)}
|
||||
errorMessage={formik.errors.total_price}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
SalesOrderProductFormValues,
|
||||
SalesOrderProductSchema,
|
||||
} from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
|
||||
import { RefObject, useState } from 'react';
|
||||
import { RefObject, useMemo, useState } from 'react';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
@@ -23,13 +23,16 @@ import Alert from '@/components/Alert';
|
||||
|
||||
const SalesOrderProductForm = ({
|
||||
initialValues,
|
||||
exisitingValues,
|
||||
onSubmitForm,
|
||||
}: {
|
||||
initialValues?: SalesOrderProductFormValues;
|
||||
exisitingValues?: SalesOrderProductFormValues[];
|
||||
modalRef?: RefObject<HTMLDialogElement | null>;
|
||||
onSubmitForm?: (value: SalesOrderProductFormValues) => Promise<void>;
|
||||
}) => {
|
||||
const [formErrorMessage, setFormErrorMessage] = useState('');
|
||||
const [currentInput, setCurrentInput] = useState<string>('');
|
||||
|
||||
const formik = useFormik<SalesOrderProductFormValues>({
|
||||
enableReinitialize: true,
|
||||
@@ -51,6 +54,8 @@ const SalesOrderProductForm = ({
|
||||
onSubmitForm?.(values);
|
||||
handleResetForm();
|
||||
},
|
||||
validateOnBlur: true,
|
||||
isInitialValid: false,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -72,6 +77,15 @@ const SalesOrderProductForm = ({
|
||||
}
|
||||
);
|
||||
|
||||
const productOptionsFiltered = useMemo(() => {
|
||||
return warehouseSourceOptions.filter(
|
||||
(product) =>
|
||||
!exisitingValues
|
||||
?.map((item) => item.product_warehouse_id)
|
||||
.includes(product.value)
|
||||
);
|
||||
}, [warehouseSourceOptions, exisitingValues]);
|
||||
|
||||
const kandangChangeHandler = (val: OptionType | OptionType[] | null) => {
|
||||
formik.setFieldValue('kandang', val as OptionType);
|
||||
formik.setFieldValue('kandang_id', (val as OptionType)?.value);
|
||||
@@ -115,6 +129,7 @@ const SalesOrderProductForm = ({
|
||||
};
|
||||
|
||||
const handleBlurField = (field: string) => {
|
||||
setCurrentInput(field);
|
||||
const { qty, unit_price, total_price, avg_weight, total_weight } =
|
||||
formik.values;
|
||||
|
||||
@@ -151,7 +166,11 @@ const SalesOrderProductForm = ({
|
||||
<>
|
||||
<form
|
||||
className='size-full'
|
||||
onSubmit={formik.handleSubmit}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleBlurField(currentInput);
|
||||
formik.handleSubmit(e);
|
||||
}}
|
||||
onReset={handleResetForm}
|
||||
>
|
||||
{formErrorMessage && (
|
||||
@@ -200,12 +219,18 @@ const SalesOrderProductForm = ({
|
||||
<SelectInput
|
||||
required
|
||||
label='Produk'
|
||||
options={warehouseSourceOptions}
|
||||
options={productOptionsFiltered}
|
||||
isLoading={isLoadingWarehouseSourceOptions}
|
||||
value={formik.values.product_warehouse}
|
||||
onChange={warehouseChangeHandler}
|
||||
isClearable
|
||||
placeholder='Pilih Kandang Terlebih Dahulu'
|
||||
placeholder={
|
||||
formik.values.kandang_id
|
||||
? productOptionsFiltered.length == 0
|
||||
? 'Tidak ada produk yang tersedia'
|
||||
: 'Pilih produk'
|
||||
: 'Pilih Kandang Terlebih Dahulu'
|
||||
}
|
||||
isDisabled={!formik.values.kandang_id}
|
||||
isError={
|
||||
formik.touched.product_warehouse_id &&
|
||||
@@ -218,7 +243,10 @@ const SalesOrderProductForm = ({
|
||||
label='Kuantitas'
|
||||
name='qty'
|
||||
value={formik.values.qty}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('qty')}
|
||||
isError={formik.touched.qty && Boolean(formik.errors.qty)}
|
||||
errorMessage={formik.errors.qty}
|
||||
@@ -229,7 +257,10 @@ const SalesOrderProductForm = ({
|
||||
label='Avg. Bobot (Kg)'
|
||||
name='avg_weight'
|
||||
value={formik.values.avg_weight}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('avg_weight')}
|
||||
isError={
|
||||
formik.touched.avg_weight && Boolean(formik.errors.avg_weight)
|
||||
@@ -242,7 +273,10 @@ const SalesOrderProductForm = ({
|
||||
label='Harga Satuan (Rp)'
|
||||
name='unit_price'
|
||||
value={formik.values.unit_price}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('unit_price')}
|
||||
isError={
|
||||
formik.touched.unit_price && Boolean(formik.errors.unit_price)
|
||||
@@ -255,7 +289,10 @@ const SalesOrderProductForm = ({
|
||||
label='Total Bobot (Kg)'
|
||||
name='total_weight'
|
||||
value={formik.values.total_weight}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('total_weight')}
|
||||
isError={
|
||||
formik.touched.total_weight && Boolean(formik.errors.total_weight)
|
||||
@@ -268,7 +305,10 @@ const SalesOrderProductForm = ({
|
||||
label='Total Penjualan (Rp)'
|
||||
name='total_price'
|
||||
value={formik.values.total_price}
|
||||
onChange={formik.handleChange}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
setCurrentInput(e.target.name);
|
||||
}}
|
||||
onBlur={() => handleBlurField('total_price')}
|
||||
isError={
|
||||
formik.touched.total_price && Boolean(formik.errors.total_price)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import Table from '@/components/Table';
|
||||
import { DeliveryOrderProductFormValues } from '../repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
|
||||
import Button from '@/components/Button';
|
||||
import { Icon } from '@iconify/react';
|
||||
import * as TanStack from '@tanstack/react-table';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import {
|
||||
cn,
|
||||
formatCurrency,
|
||||
@@ -12,49 +11,24 @@ import {
|
||||
formatNumber,
|
||||
formatVechicleNumber,
|
||||
} from '@/lib/helper';
|
||||
import { SalesOrderProductFormValues } from '../repeater/sales-order/SalesOrderProduct.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
|
||||
type DeliveryOrderProductTableProps = {
|
||||
data: DeliveryOrderProductFormValues[];
|
||||
salesOrder: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
>;
|
||||
selectedRowIds: number[];
|
||||
onDelete: (id: number) => void;
|
||||
onEdit: (id: number) => void;
|
||||
onBulkDelete: () => void;
|
||||
onAddProductClick: () => void;
|
||||
onInputDate: (data: DeliveryOrderProductFormValues) => void;
|
||||
};
|
||||
|
||||
const DeliveryOrderProductTable = ({
|
||||
data,
|
||||
salesOrder,
|
||||
formType,
|
||||
rowSelection,
|
||||
setRowSelection,
|
||||
selectedRowIds,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onBulkDelete,
|
||||
onAddProductClick,
|
||||
onInputDate,
|
||||
}: DeliveryOrderProductTableProps) => {
|
||||
const onDeleteRef = useRef(onDelete);
|
||||
const onEditRef = useRef(onDelete);
|
||||
onDeleteRef.current = onDelete;
|
||||
const onEditRef = useRef(onEdit);
|
||||
onEditRef.current = onEdit;
|
||||
|
||||
const canAddData = salesOrder.reduce((acc, curr) => {
|
||||
const deliveredQty = data.filter(
|
||||
(deliveryItem) => deliveryItem.marketing_product_id == curr.id
|
||||
);
|
||||
return acc && deliveredQty.length != salesOrder.length;
|
||||
}, true);
|
||||
const canAddData = data.filter((item) => !Boolean(item.qty));
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const cols = [
|
||||
@@ -93,54 +67,23 @@ const DeliveryOrderProductTable = ({
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.do_number,
|
||||
header: 'No. Pengiriman',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => <div>{props.row.original.do_number}</div>,
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
row.delivery_date
|
||||
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
|
||||
: '-',
|
||||
header: 'Tanggal Delivery',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) => (
|
||||
<>
|
||||
{formType == 'add_deliver' && (
|
||||
<DateInput
|
||||
name={`delivery_date_${props.row.original.marketing_product_id}`}
|
||||
className={{
|
||||
input: 'p-0',
|
||||
inputWrapper: 'py-1 px-3 h-fit w-fit bg-white',
|
||||
wrapper: 'p-0',
|
||||
}}
|
||||
value={
|
||||
props.row.original.delivery_date
|
||||
? formatDate(props.row.original.delivery_date, 'yyyy-MM-DD')
|
||||
: undefined
|
||||
}
|
||||
onChange={(val) => {
|
||||
onInputDate({
|
||||
...props.row.original,
|
||||
delivery_date: val.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formType == 'edit_deliver' &&
|
||||
formatDate(
|
||||
props.row.original.delivery_date as string,
|
||||
'DD MMM YYYY'
|
||||
)}
|
||||
{props.row.original.do_number ? props.row.original.do_number : '-'}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatVechicleNumber(row.vehicle_number as string),
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.vehicle_number,
|
||||
header: 'No. Polisi',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.vehicle_number
|
||||
? formatVechicleNumber(props.row.original.vehicle_number as string)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
@@ -154,29 +97,77 @@ const DeliveryOrderProductTable = ({
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.unit_price as string)),
|
||||
row.delivery_date
|
||||
? formatDate(row.delivery_date as string, 'DD MMM YYYY')
|
||||
: '-',
|
||||
header: 'Tanggal Delivery',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.delivery_date
|
||||
? formatDate(
|
||||
props.row.original.delivery_date as string,
|
||||
'DD MMM YYYY'
|
||||
)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.unit_price,
|
||||
header: 'Harga Satuan (Rp)',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.unit_price
|
||||
? formatCurrency(
|
||||
parseFloat(props.row.original.unit_price as string)
|
||||
)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.total_weight as string)),
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.total_weight,
|
||||
header: 'Total Bobot (Kg)',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.total_weight
|
||||
? formatNumber(
|
||||
parseFloat(props.row.original.total_weight as string)
|
||||
)
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.qty as string)),
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.qty,
|
||||
header: 'Kuantitas',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.qty
|
||||
? formatNumber(parseFloat(props.row.original.qty as string))
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatNumber(parseFloat(row.avg_weight as string)),
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.avg_weight,
|
||||
header: 'Avg. Bobot (Kg)',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.avg_weight
|
||||
? formatNumber(parseFloat(props.row.original.avg_weight as string))
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) =>
|
||||
formatCurrency(parseFloat(row.total_price as string)),
|
||||
accessorFn: (row: DeliveryOrderProductFormValues) => row.total_price,
|
||||
header: 'Total Penjualan (Rp)',
|
||||
cell: (
|
||||
props: TanStack.CellContext<DeliveryOrderProductFormValues, unknown>
|
||||
) =>
|
||||
props.row.original.total_price
|
||||
? formatCurrency(
|
||||
parseFloat(props.row.original.total_price as string)
|
||||
)
|
||||
: '-',
|
||||
},
|
||||
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (
|
||||
@@ -184,44 +175,45 @@ const DeliveryOrderProductTable = ({
|
||||
) => (
|
||||
<div className='flex flex-row gap-1 items-center justify-end h-full mt-2'>
|
||||
<>
|
||||
<Button
|
||||
color='warning'
|
||||
className='px-2 py-1 text-sm'
|
||||
onClick={() =>
|
||||
onEditRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
{/* <Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button> */}
|
||||
{props.row.original.qty && (
|
||||
<Button
|
||||
color='warning'
|
||||
className='px-2 py-1 text-sm'
|
||||
onClick={() =>
|
||||
onEditRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:edit' width={16} height={16} /> Edit
|
||||
</Button>
|
||||
)}
|
||||
{!props.row.original.qty && '-'}
|
||||
{/* {formType == 'add_deliver' && (
|
||||
<Button
|
||||
color='error'
|
||||
className='p-1'
|
||||
onClick={() =>
|
||||
onDeleteRef.current(props.row.original.id as number)
|
||||
}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='mdi:trash' width={16} height={16} />
|
||||
</Button>
|
||||
)} */}
|
||||
</>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
if (formType == 'add_deliver') {
|
||||
return cols.filter(
|
||||
(col) => col.header != 'Aksi' && col.header != 'No. Pengiriman'
|
||||
);
|
||||
return cols.filter((col) => col.header != 'No. Pengiriman');
|
||||
}
|
||||
return cols;
|
||||
}, [formType, onInputDate, onEditRef]);
|
||||
}, [formType, onEditRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table<DeliveryOrderProductFormValues>
|
||||
rowSelection={rowSelection}
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={columns}
|
||||
className={{
|
||||
@@ -246,17 +238,17 @@ const DeliveryOrderProductTable = ({
|
||||
}
|
||||
/>
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
{/* <Button
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
className='justify-start w-fit py-1 text-sm'
|
||||
onClick={onAddProductClick}
|
||||
// disabled={!canAddData}
|
||||
disabled={!canAddData}
|
||||
>
|
||||
<Icon icon='mdi:plus' width={16} height={16} />
|
||||
Tambah Pengiriman
|
||||
</Button> */}
|
||||
{selectedRowIds.length > 0 && (
|
||||
</Button>
|
||||
{/* {selectedRowIds.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
@@ -271,7 +263,7 @@ const DeliveryOrderProductTable = ({
|
||||
: ''}{' '}
|
||||
Pengiriman
|
||||
</Button>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
|
||||
type SalesOrderProductTableProps = {
|
||||
data: SalesOrderProductFormValues[];
|
||||
formType?: 'add' | 'edit' | 'deliver';
|
||||
formType: 'add' | 'edit' | 'add_deliver' | 'edit_deliver';
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: React.Dispatch<
|
||||
React.SetStateAction<Record<string, boolean>>
|
||||
@@ -140,7 +140,7 @@ const SalesOrderProductTable = ({
|
||||
setRowSelection={setRowSelection}
|
||||
data={data}
|
||||
columns={
|
||||
formType == 'deliver'
|
||||
formType == 'add_deliver' || formType == 'edit_deliver'
|
||||
? columns.filter(
|
||||
(col) => col.header != 'Aksi' && col.id != 'select'
|
||||
)
|
||||
@@ -167,7 +167,7 @@ const SalesOrderProductTable = ({
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{formType != 'deliver' && (
|
||||
{formType != 'add_deliver' && formType != 'edit_deliver' && (
|
||||
<div className='flex flex-row gap-3 mt-3'>
|
||||
<Button
|
||||
type='button'
|
||||
|
||||
+3
-1
@@ -10,7 +10,9 @@ export const ProductCategoryFormSchema: Yup.ObjectSchema<ProductCategoryFormSche
|
||||
code: Yup.string()
|
||||
.required('Kode wajib diisi!')
|
||||
.max(3, 'Kode kategori produk melebihi 3 karakter!'),
|
||||
name: Yup.string().required('Nama wajib diisi!'),
|
||||
name: Yup.string()
|
||||
.required('Nama wajib diisi!')
|
||||
.max(50, 'Nama kategori produk melebihi 50 karakter!'),
|
||||
});
|
||||
|
||||
export const UpdateProductCategoryFormSchema = ProductCategoryFormSchema;
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ChickinFormValues,
|
||||
ChickinRequestFormValues,
|
||||
ChickinSchema,
|
||||
} from '../ChickinForm.schema';
|
||||
} from '@/components/pages/production/chickin/form/ChickinForm.schema';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@@ -42,6 +42,7 @@ import ApprovalSteps, {
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import { PROJECT_FLOCK_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
|
||||
interface ProjectFlockFormProps {
|
||||
formType?: 'add' | 'edit' | 'detail';
|
||||
@@ -548,10 +549,18 @@ const ProjectFlockForm = ({
|
||||
setIsApproveLoading(false);
|
||||
};
|
||||
|
||||
const selectedPeriod = isResponseSuccess(periodFlocks)
|
||||
? periodFlocks.data.find((kandang) =>
|
||||
formik.values.kandang_ids?.includes(kandang.id)
|
||||
)?.period
|
||||
: undefined;
|
||||
const inputPeriod =
|
||||
(initialValues?.period ?? selectedPeriod == 0) ? 1 : selectedPeriod;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<header className='flex flex-col gap-4 mb-6'>
|
||||
<Button
|
||||
href='/production/project-flock'
|
||||
variant='link'
|
||||
@@ -563,6 +572,7 @@ const ProjectFlockForm = ({
|
||||
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
{formType === 'add' && 'Tambah Project Flock'}
|
||||
{formType === 'edit' && 'Edit Project Flock'}
|
||||
{formType === 'detail' && 'Detail Project Flock'}
|
||||
</h1>
|
||||
</header>
|
||||
@@ -740,6 +750,14 @@ const ProjectFlockForm = ({
|
||||
isClearable
|
||||
isDisabled={formType === 'detail'}
|
||||
/>
|
||||
<NumberInput
|
||||
name='period'
|
||||
label='Periode'
|
||||
disabled
|
||||
readOnly
|
||||
placeholder='Period'
|
||||
value={selectedLocation ? inputPeriod : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -781,7 +799,7 @@ const ProjectFlockForm = ({
|
||||
setRowSelection={setRowSelection}
|
||||
selectedIds={formik.values.kandang_ids}
|
||||
formType={formType}
|
||||
initialValues={initialValues?.kandangs ?? []}
|
||||
initialValues={initialValues}
|
||||
/>
|
||||
</div>
|
||||
</Collapse>
|
||||
|
||||
@@ -5,7 +5,10 @@ import PillBadge from '@/components/PillBadge';
|
||||
import Table from '@/components/Table';
|
||||
import { cn } from '@/lib/helper';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { ProjectFlockPeriods } from '@/types/api/production/project-flock';
|
||||
import {
|
||||
ProjectFlock,
|
||||
ProjectFlockPeriods,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import { OnChangeFn, Row } from '@tanstack/react-table';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -23,11 +26,11 @@ const ProjectFlockKandangTable = ({
|
||||
rowSelection: Record<string, boolean>;
|
||||
setRowSelection: OnChangeFn<Record<string, boolean>>;
|
||||
selectedIds: (number | undefined)[];
|
||||
initialValues?: Kandang[];
|
||||
initialValues?: ProjectFlock;
|
||||
formType: 'add' | 'edit' | 'detail';
|
||||
}) => {
|
||||
const initialKandangIdSet = useMemo(() => {
|
||||
return initialValues?.map((k) => k.id) ?? [];
|
||||
return initialValues?.kandangs.map((k) => k.id) ?? [];
|
||||
}, [initialValues]);
|
||||
const isRowEnabled = (row: Row<Kandang>) => {
|
||||
const isDisabled =
|
||||
@@ -147,7 +150,18 @@ const ProjectFlockKandangTable = ({
|
||||
listPeriods.length > 0
|
||||
? listPeriods.find((p) => p.id == props.row.original.id)
|
||||
: undefined;
|
||||
return period?.period ?? '-';
|
||||
const calcPeriod = period?.period == 0 ? 1 : period?.period;
|
||||
const selected = props.row.getIsSelected();
|
||||
const initPeriod = initialValues?.period;
|
||||
return formType == 'detail'
|
||||
? selected
|
||||
? initPeriod
|
||||
: '-'
|
||||
: formType == 'add'
|
||||
? (calcPeriod ?? '-')
|
||||
: selected
|
||||
? (initPeriod ?? '-')
|
||||
: (calcPeriod ?? '-');
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
'use client';
|
||||
|
||||
import { ChangeEventHandler, useCallback, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import { Icon } from '@iconify/react';
|
||||
import Table from '@/components/Table';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import { useModal } from '@/components/Modal';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import SelectInput, { OptionType } from '@/components/input/SelectInput';
|
||||
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
|
||||
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
|
||||
import { cn, formatDate, formatCurrency } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
import { ROWS_OPTIONS } from '@/config/constant';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
|
||||
// ===== INTERFACES =====
|
||||
interface RowOptionsMenuProps {
|
||||
type: 'dropdown' | 'collapse';
|
||||
props: CellContext<Purchase, unknown>;
|
||||
deleteClickHandler: () => void;
|
||||
}
|
||||
|
||||
const RowOptionsMenu = ({
|
||||
type = 'dropdown',
|
||||
props,
|
||||
deleteClickHandler,
|
||||
}: RowOptionsMenuProps) => {
|
||||
return (
|
||||
<RowOptionsMenuWrapper type={type}>
|
||||
<Button
|
||||
href={`/purchase/detail/?purchaseId=${props.row.original.id}`}
|
||||
variant='ghost'
|
||||
color='primary'
|
||||
className='justify-start text-sm'
|
||||
>
|
||||
<Icon icon='mdi:eye-outline' width={16} height={16} />
|
||||
Detail
|
||||
</Button>
|
||||
|
||||
{/*<Button*/}
|
||||
{/* href={`/purchase/detail/edit/?purchaseId=${props.row.original.id}`}*/}
|
||||
{/* variant='ghost'*/}
|
||||
{/* color='warning'*/}
|
||||
{/* className='justify-start text-sm'*/}
|
||||
{/*>*/}
|
||||
{/* <Icon icon='material-symbols:edit-outline' width={16} height={16} />*/}
|
||||
{/* Edit*/}
|
||||
{/*</Button>*/}
|
||||
|
||||
<Button
|
||||
onClick={deleteClickHandler}
|
||||
variant='ghost'
|
||||
color='error'
|
||||
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={16}
|
||||
height={16}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</RowOptionsMenuWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const PurchaseTable = () => {
|
||||
// ===== STATE MANAGEMENT =====
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [selectedPurchase, setSelectedPurchase] = useState<Purchase | null>(
|
||||
null
|
||||
);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
// ===== TABLE FILTER STATE =====
|
||||
const {
|
||||
state: tableFilterState,
|
||||
updateFilter,
|
||||
setPage,
|
||||
setPageSize,
|
||||
toQueryString: getTableFilterQueryString,
|
||||
} = useTableFilter({
|
||||
initial: {
|
||||
search: '',
|
||||
},
|
||||
paramMap: {
|
||||
page: 'page',
|
||||
pageSize: 'limit',
|
||||
},
|
||||
});
|
||||
|
||||
// ===== MODAL HOOKS =====
|
||||
const deleteModal = useModal();
|
||||
|
||||
// ===== API DATA FETCHING =====
|
||||
const {
|
||||
data: purchaseRequests,
|
||||
isLoading,
|
||||
mutate: refreshPurchaseRequests,
|
||||
} = useSWR(
|
||||
`${PurchaseApi.basePath}${getTableFilterQueryString()}`,
|
||||
PurchaseApi.getAllFetcher
|
||||
);
|
||||
|
||||
// ===== TABLE COLUMNS DEFINITION =====
|
||||
const purchaseColumns: ColumnDef<Purchase>[] = [
|
||||
{
|
||||
header: 'No. PR/PO',
|
||||
cell: (props) => {
|
||||
const { pr_number, po_number } = props.row.original;
|
||||
return po_number ? po_number : pr_number;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'supplier',
|
||||
header: 'Vendor',
|
||||
cell: (props) => props.row.original.supplier.name,
|
||||
},
|
||||
{
|
||||
accessorKey: 'po_date',
|
||||
header: 'Tgl. PO',
|
||||
cell: (props) =>
|
||||
props.row.original.po_date
|
||||
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'due_date',
|
||||
header: 'Jatuh Tempo',
|
||||
cell: (props) =>
|
||||
props.row.original.due_date
|
||||
? formatDate(props.row.original.due_date, 'DD MMM YYYY')
|
||||
: '-',
|
||||
},
|
||||
{
|
||||
header: 'Aging',
|
||||
cell: (props) => {
|
||||
const purchase = props.row.original;
|
||||
if (!purchase.po_date) return '-';
|
||||
const poDate = new Date(purchase.po_date);
|
||||
const today = new Date();
|
||||
const diffTime = Math.abs(today.getTime() - poDate.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
return `${diffDays} hari`;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'grand_total',
|
||||
header: 'Total (Rp.)',
|
||||
cell: (props) => formatCurrency(props.row.original.grand_total),
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props) => {
|
||||
const currentPageSize = props.table.getPaginationRowModel().rows.length;
|
||||
const currentPageRows = props.table.getPaginationRowModel().flatRows;
|
||||
const currentRowRelativeIndex =
|
||||
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
|
||||
|
||||
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
|
||||
|
||||
const deleteClickHandler = () => {
|
||||
setSelectedPurchase(props.row.original);
|
||||
deleteModal.openModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{currentPageSize > 2 && (
|
||||
<RowDropdownOptions isLast2Rows={isLast2Rows}>
|
||||
<RowOptionsMenu
|
||||
type='dropdown'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowDropdownOptions>
|
||||
)}
|
||||
|
||||
{currentPageSize <= 2 && (
|
||||
<RowCollapseOptions>
|
||||
<RowOptionsMenu
|
||||
type='collapse'
|
||||
props={props}
|
||||
deleteClickHandler={deleteClickHandler}
|
||||
/>
|
||||
</RowCollapseOptions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
const confirmationModalDeleteClickHandler = useCallback(async () => {
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
try {
|
||||
await PurchaseApi.delete(selectedPurchase?.id as number);
|
||||
refreshPurchaseRequests();
|
||||
deleteModal.closeModal();
|
||||
toast.success('Berhasil menghapus data permintaan pembelian!');
|
||||
} catch {
|
||||
toast.error('Gagal menghapus data permintaan pembelian!');
|
||||
}
|
||||
|
||||
setIsDeleteLoading(false);
|
||||
}, [selectedPurchase?.id, refreshPurchaseRequests, deleteModal]);
|
||||
|
||||
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
updateFilter('search', e.target.value);
|
||||
},
|
||||
[updateFilter]
|
||||
);
|
||||
|
||||
const pageSizeChangeHandler = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const newVal = val as OptionType;
|
||||
setPageSize(newVal.value as number);
|
||||
},
|
||||
[setPageSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full p-0 sm:p-4'>
|
||||
<div className='flex flex-col gap-2 mb-4'>
|
||||
<div className='w-full flex flex-col xl:flex-row justify-between items-end xl:items-center gap-2'>
|
||||
<div className='w-full flex flex-row gap-2'>
|
||||
<Button
|
||||
href='/purchase/add'
|
||||
variant='outline'
|
||||
color='primary'
|
||||
className='w-full sm:w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<DebouncedTextInput
|
||||
name='search'
|
||||
placeholder='Cari Pembelian'
|
||||
value={tableFilterState.search}
|
||||
onChange={searchChangeHandler}
|
||||
className={{
|
||||
wrapper: 'sm:max-w-3xs',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap justify-end gap-4'>
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={ROWS_OPTIONS}
|
||||
value={{
|
||||
label: String(tableFilterState.pageSize),
|
||||
value: tableFilterState.pageSize,
|
||||
}}
|
||||
onChange={pageSizeChangeHandler}
|
||||
className={{
|
||||
wrapper: 'w-full sm:w-24',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table<Purchase>
|
||||
data={
|
||||
isResponseSuccess(purchaseRequests) ? purchaseRequests?.data : []
|
||||
}
|
||||
columns={purchaseColumns}
|
||||
pageSize={tableFilterState.pageSize}
|
||||
page={
|
||||
isResponseSuccess(purchaseRequests)
|
||||
? purchaseRequests?.meta?.page
|
||||
: 0
|
||||
}
|
||||
totalItems={
|
||||
isResponseSuccess(purchaseRequests)
|
||||
? purchaseRequests?.meta?.total_results
|
||||
: 0
|
||||
}
|
||||
onPageChange={setPage}
|
||||
isLoading={isLoading}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
className={{
|
||||
containerClassName: cn({
|
||||
'mb-20':
|
||||
isResponseSuccess(purchaseRequests) &&
|
||||
purchaseRequests?.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 last:flex last:flex-row last:justify-end',
|
||||
bodyRowClassName: 'border-b border-b-gray-200',
|
||||
bodyColumnClassName:
|
||||
'px-6 py-3 last:flex last:flex-row last:justify-end',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ===== MODAL COMPONENTS ===== */}
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text={`Apakah anda yakin ingin menghapus data permintaan pembelian ini?`}
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseTable;
|
||||
@@ -0,0 +1,758 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import {
|
||||
PurchaseRequestAcceptApprovalFormDefaultValues,
|
||||
PurchaseRequestAcceptApprovalFormInitialValues,
|
||||
PurchaseRequestAcceptApprovalFormSchema,
|
||||
} from './PurchaseOrderForm.schema';
|
||||
import { isResponseError } from '@/lib/api-helper';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
import {
|
||||
CreateAcceptApprovalRequestPayload,
|
||||
Purchase,
|
||||
} from '@/types/api/purchase/purchase';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { SupplierApi } from '@/services/api/master-data';
|
||||
|
||||
interface PurchaseOrderAcceptApprovalFormProps {
|
||||
type?: 'add' | 'edit';
|
||||
initialValues?: Purchase;
|
||||
onCancel?: () => void;
|
||||
refreshApprovals?: () => void;
|
||||
onModalClose?: () => void;
|
||||
onRefetchData?: () => void;
|
||||
}
|
||||
|
||||
const PurchaseOrderAcceptApprovalForm = ({
|
||||
type = 'add',
|
||||
initialValues,
|
||||
onCancel,
|
||||
refreshApprovals,
|
||||
onModalClose,
|
||||
onRefetchData,
|
||||
}: PurchaseOrderAcceptApprovalFormProps) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
|
||||
useState('');
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const isRepeaterInputError = (
|
||||
idx: number,
|
||||
field:
|
||||
| 'purchase_item_id'
|
||||
| 'received_date'
|
||||
| 'travel_number'
|
||||
| 'travel_document_path'
|
||||
| 'vehicle_number'
|
||||
| 'expedition_vendor_id'
|
||||
| 'received_qty'
|
||||
| 'transport_per_item'
|
||||
| 'transport_total'
|
||||
): { isError: boolean; errorMessage: string } => {
|
||||
const touchedItem = formik.touched.items?.[idx];
|
||||
const errorItem = formik.errors.items?.[idx] as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
|
||||
if (!touchedItem) {
|
||||
return { isError: false, errorMessage: '' };
|
||||
}
|
||||
|
||||
const isTouched = (touchedItem as Record<string, boolean>)?.[field];
|
||||
const errorMessage = errorItem?.[field] || '';
|
||||
|
||||
return {
|
||||
isError: Boolean(isTouched && errorMessage),
|
||||
errorMessage: isTouched && errorMessage ? errorMessage : '',
|
||||
};
|
||||
};
|
||||
|
||||
// ===== SUBMISSION HANDLERS =====
|
||||
const createAcceptApprovalHandler = useCallback(
|
||||
async (payload: CreateAcceptApprovalRequestPayload) => {
|
||||
const purchaseRequestId = searchParams.get('purchaseId')
|
||||
? parseInt(searchParams.get('purchaseId')!)
|
||||
: initialValues?.id || 1;
|
||||
|
||||
if (!purchaseRequestId) {
|
||||
setPurchaseOrderFormErrorMessage('Purchase Request ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await PurchaseApi.acceptApproval.create(
|
||||
purchaseRequestId,
|
||||
payload
|
||||
);
|
||||
|
||||
if (isResponseError(res)) {
|
||||
setPurchaseOrderFormErrorMessage(res.message);
|
||||
return;
|
||||
}
|
||||
toast.success(res?.message as string);
|
||||
refreshApprovals?.();
|
||||
onRefetchData?.();
|
||||
formik.resetForm();
|
||||
onCancel?.();
|
||||
onModalClose?.();
|
||||
router.refresh();
|
||||
},
|
||||
[
|
||||
initialValues?.id,
|
||||
searchParams,
|
||||
refreshApprovals,
|
||||
onModalClose,
|
||||
onRefetchData,
|
||||
]
|
||||
);
|
||||
|
||||
const updateAcceptApprovalHandler = useCallback(
|
||||
async (purchaseId: number, payload: CreateAcceptApprovalRequestPayload) => {
|
||||
const res = await PurchaseApi.acceptApproval.create(purchaseId, payload);
|
||||
if (isResponseError(res)) {
|
||||
setPurchaseOrderFormErrorMessage(res.message);
|
||||
return;
|
||||
}
|
||||
toast.success(res?.message as string);
|
||||
refreshApprovals?.();
|
||||
onRefetchData?.();
|
||||
formik.resetForm();
|
||||
onCancel?.();
|
||||
onModalClose?.();
|
||||
router.refresh();
|
||||
},
|
||||
[refreshApprovals, onModalClose, onRefetchData]
|
||||
);
|
||||
|
||||
// ===== SELECT INPUT DATA =====
|
||||
const {
|
||||
setInputValue: setExpeditionsSelectInputValue,
|
||||
options: expeditionVendors,
|
||||
isLoadingOptions: isLoadingExpeditions,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'BOP',
|
||||
});
|
||||
|
||||
// ===== FORM CONFIGURATION =====
|
||||
const formikInitialValues = useMemo(() => {
|
||||
return initialValues
|
||||
? PurchaseRequestAcceptApprovalFormDefaultValues(initialValues)
|
||||
: PurchaseRequestAcceptApprovalFormInitialValues;
|
||||
}, [initialValues]);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema: PurchaseRequestAcceptApprovalFormSchema,
|
||||
validateOnChange: true,
|
||||
validateOnBlur: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
notes: values.notes || '',
|
||||
items:
|
||||
values.items?.map((formItem) => {
|
||||
return {
|
||||
purchase_item_id: formItem.purchase_item_id || 0,
|
||||
received_date: formItem.received_date || '',
|
||||
travel_number: formItem.travel_number || '',
|
||||
travel_document_path: formItem.travel_document_path || '',
|
||||
vehicle_number: formItem.vehicle_number || '',
|
||||
expedition_vendor_id: formItem.expedition_vendor_id || 0,
|
||||
received_qty:
|
||||
typeof formItem.received_qty === 'string'
|
||||
? parseFloat(formItem.received_qty) || 0
|
||||
: formItem.received_qty || 0,
|
||||
transport_per_item:
|
||||
typeof formItem.transport_per_item === 'string'
|
||||
? parseFloat(formItem.transport_per_item) || 0
|
||||
: formItem.transport_per_item || 0,
|
||||
transport_total:
|
||||
typeof formItem.transport_total === 'string'
|
||||
? parseFloat(formItem.transport_total) || 0
|
||||
: formItem.transport_total || 0,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'add':
|
||||
await createAcceptApprovalHandler(payload);
|
||||
break;
|
||||
case 'edit':
|
||||
await updateAcceptApprovalHandler(
|
||||
initialValues?.id as number,
|
||||
payload
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ===== API DATA FETCHING =====
|
||||
const purchaseItems = useMemo(() => {
|
||||
if (initialValues?.items) {
|
||||
return initialValues.items.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.product.name,
|
||||
id: item.id,
|
||||
quantity: item.sub_qty,
|
||||
product: {
|
||||
name: item.product.name,
|
||||
product_category: item.product.product_category || '',
|
||||
uom: item.product.uom || { name: '' },
|
||||
},
|
||||
warehouse: {
|
||||
name: item.warehouse?.name || '',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [initialValues?.items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (purchaseItems.length > 0 && initialValues?.items) {
|
||||
const updatedItems = initialValues.items.map((item) => {
|
||||
return {
|
||||
purchase_item: null,
|
||||
purchase_item_id: item.id,
|
||||
received_date: item.received_date
|
||||
? new Date(item.received_date).toISOString().split('T')[0]
|
||||
: '',
|
||||
travel_number: item.travel_number || '',
|
||||
travel_document_path: item.travel_document_path || '',
|
||||
vehicle_number: item.vehicle_number || '',
|
||||
expedition_vendor: null,
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
};
|
||||
});
|
||||
formik.setFieldValue('items', updatedItems);
|
||||
}
|
||||
}, [purchaseItems, initialValues]);
|
||||
|
||||
// ===== HELPER FUNCTIONS =====
|
||||
const getQuantityExceededError = useCallback(
|
||||
(idx: number, receivedQty: number) => {
|
||||
if (!receivedQty) return null;
|
||||
|
||||
const originalQty = purchaseItems[idx]?.quantity || 0;
|
||||
if (receivedQty > originalQty) {
|
||||
return `Tidak boleh melebihi ${formatNumber(originalQty)}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[purchaseItems]
|
||||
);
|
||||
|
||||
const hasQuantityExceededErrors = useMemo(() => {
|
||||
if (!formik.values.items || purchaseItems.length === 0) return false;
|
||||
|
||||
return formik.values.items.some((item, idx) => {
|
||||
if (!item.received_qty) return false;
|
||||
|
||||
const receivedQty =
|
||||
typeof item.received_qty === 'string'
|
||||
? parseFloat(item.received_qty) || 0
|
||||
: item.received_qty;
|
||||
|
||||
const originalQty = purchaseItems[idx]?.quantity || 0;
|
||||
return receivedQty > originalQty;
|
||||
});
|
||||
}, [formik.values.items, purchaseItems]);
|
||||
|
||||
const getExpeditionVendorOptions = useCallback(() => {
|
||||
return expeditionVendors;
|
||||
}, [expeditionVendors]);
|
||||
|
||||
// ===== FIELD CHANGE HANDLERS =====
|
||||
const expeditionVendorChangeHandler = (
|
||||
idx: number,
|
||||
val: OptionType | OptionType[] | null
|
||||
) => {
|
||||
const expeditionVendor = val as OptionType | null;
|
||||
formik.setFieldTouched(`items.${idx}.expedition_vendor`, true);
|
||||
formik.setFieldValue(`items.${idx}.expedition_vendor`, expeditionVendor);
|
||||
formik.setFieldTouched(`items.${idx}.expedition_vendor_id`, true);
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.expedition_vendor_id`,
|
||||
expeditionVendor?.value || 0
|
||||
);
|
||||
};
|
||||
|
||||
// ===== PURCHASE ITEM OPERATIONS =====
|
||||
const handlePurchaseItemChange = (
|
||||
idx: number,
|
||||
field: 'received_qty' | 'transport_per_item' | 'transport_total',
|
||||
value: string | number
|
||||
) => {
|
||||
const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value;
|
||||
formik.setFieldValue(`items.${idx}.${field}`, numValue);
|
||||
|
||||
if (field === 'received_qty' || field === 'transport_per_item') {
|
||||
const receivedQty =
|
||||
field === 'received_qty'
|
||||
? numValue
|
||||
: parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0;
|
||||
const transportPerItem =
|
||||
field === 'transport_per_item'
|
||||
? numValue
|
||||
: parseFloat(
|
||||
formik.values.items?.[idx]?.transport_per_item as string
|
||||
) || 0;
|
||||
|
||||
if (receivedQty > 0 && transportPerItem >= 0) {
|
||||
const calculatedTransportTotal = receivedQty * transportPerItem;
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.transport_total`,
|
||||
calculatedTransportTotal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (field === 'transport_total') {
|
||||
const receivedQty =
|
||||
parseFloat(formik.values.items?.[idx]?.received_qty as string) || 0;
|
||||
if (receivedQty > 0 && numValue >= 0) {
|
||||
const calculatedTransportPerItem = numValue / receivedQty;
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.transport_per_item`,
|
||||
calculatedTransportPerItem
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={formik.handleSubmit} className='w-full flex flex-col gap-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='text-lg font-semibold mb-4'>
|
||||
{type === 'add'
|
||||
? 'Konfirmasi Penerimaan Produk'
|
||||
: 'Edit Penerimaan Produk'}
|
||||
</h2>
|
||||
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produk</th>
|
||||
<th>Gudang</th>
|
||||
<th>Jumlah</th>
|
||||
<th>Satuan</th>
|
||||
<th>
|
||||
Tanggal Diterima
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
No. Surat Jalan
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Dokumen Surat Jalan
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Nomor Kendaraan
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Vendor Ekspedisi
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Jumlah Diterima
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Transport/Item
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Total Transport
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{purchaseItems?.map((purchaseItem, idx) => {
|
||||
const formItem = formik.values.items?.[idx];
|
||||
return (
|
||||
<tr key={`purchase-item-${idx}`}>
|
||||
<td>
|
||||
<TextInput
|
||||
name={`items.${idx}.product_name`}
|
||||
type='text'
|
||||
value={purchaseItem?.product?.name || ''}
|
||||
readOnly={true}
|
||||
className={{
|
||||
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80',
|
||||
}}
|
||||
disabled={true}
|
||||
/>
|
||||
<input
|
||||
type='hidden'
|
||||
name={`items.${idx}.purchase_item_id`}
|
||||
value={purchaseItem?.value || 0}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
name={`items.${idx}.warehouse`}
|
||||
type='text'
|
||||
value={purchaseItem?.warehouse?.name || ''}
|
||||
readOnly={true}
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
disabled={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
name={`items.${idx}.quantity`}
|
||||
type='text'
|
||||
value={
|
||||
purchaseItem?.quantity
|
||||
? formatNumber(purchaseItem.quantity)
|
||||
: ''
|
||||
}
|
||||
readOnly={true}
|
||||
className={{
|
||||
wrapper: 'min-w-32',
|
||||
}}
|
||||
disabled={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
name={`items.${idx}.uom`}
|
||||
type='text'
|
||||
value={purchaseItem?.product?.uom?.name || ''}
|
||||
readOnly={true}
|
||||
className={{
|
||||
wrapper: 'min-w-24',
|
||||
}}
|
||||
disabled={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<DateInput
|
||||
required
|
||||
isNestedModal={true}
|
||||
name={`items.${idx}.received_date`}
|
||||
value={formItem?.received_date || ''}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.received_date`,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'received_date').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'received_date')
|
||||
.errorMessage
|
||||
}
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.travel_number`}
|
||||
type='text'
|
||||
value={formItem?.travel_number || ''}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.travel_number`,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'travel_number').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'travel_number')
|
||||
.errorMessage
|
||||
}
|
||||
placeholder='Masukkan no. surat jalan'
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.travel_document_path`}
|
||||
type='text'
|
||||
value={formItem?.travel_document_path || ''}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.travel_document_path`,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'travel_document_path')
|
||||
.isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'travel_document_path')
|
||||
.errorMessage
|
||||
}
|
||||
placeholder='Masukkan path dokumen'
|
||||
className={{
|
||||
wrapper: 'min-w-52 md:min-w-72 lg:min-w-80',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.vehicle_number`}
|
||||
type='text'
|
||||
value={formItem?.vehicle_number || ''}
|
||||
onChange={(e) =>
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.vehicle_number`,
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'vehicle_number').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'vehicle_number')
|
||||
.errorMessage
|
||||
}
|
||||
placeholder='Masukkan nomor kendaraan'
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SelectInput
|
||||
required
|
||||
isClearable={true}
|
||||
value={formItem?.expedition_vendor}
|
||||
key={`expedition-vendor-${idx}`}
|
||||
onChange={(val) =>
|
||||
expeditionVendorChangeHandler(idx, val)
|
||||
}
|
||||
options={getExpeditionVendorOptions()}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'expedition_vendor_id')
|
||||
.isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'expedition_vendor_id')
|
||||
.errorMessage
|
||||
}
|
||||
placeholder='Pilih Vendor...'
|
||||
className={{
|
||||
wrapper: 'min-w-48 md:min-w-64 lg:min-w-72',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`items.${idx}.received_qty`}
|
||||
value={formItem?.received_qty || ''}
|
||||
onChange={(e) =>
|
||||
handlePurchaseItemChange(
|
||||
idx,
|
||||
'received_qty',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder='Masukkan jumlah diterima'
|
||||
allowNegative={false}
|
||||
decimalScale={0}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
bottomLabel={`Total: ${purchaseItems[idx]?.quantity ? formatNumber(purchaseItems[idx].quantity) : 0}`}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'received_qty').isError ||
|
||||
(formItem?.received_qty
|
||||
? getQuantityExceededError(
|
||||
idx,
|
||||
Number(formItem.received_qty)
|
||||
) !== null
|
||||
: false)
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'received_qty')
|
||||
.errorMessage ||
|
||||
(formItem?.received_qty
|
||||
? getQuantityExceededError(
|
||||
idx,
|
||||
Number(formItem.received_qty)
|
||||
) || undefined
|
||||
: undefined)
|
||||
}
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`items.${idx}.transport_per_item`}
|
||||
value={formItem?.transport_per_item || ''}
|
||||
onChange={(e) =>
|
||||
handlePurchaseItemChange(
|
||||
idx,
|
||||
'transport_per_item',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder='Masukkan transport/item'
|
||||
allowNegative={false}
|
||||
decimalScale={2}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputPrefix={'Rp'}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'transport_per_item')
|
||||
.isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'transport_per_item')
|
||||
.errorMessage
|
||||
}
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`items.${idx}.transport_total`}
|
||||
value={formItem?.transport_total || ''}
|
||||
onChange={(e) =>
|
||||
handlePurchaseItemChange(
|
||||
idx,
|
||||
'transport_total',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder='Masukkan total transport'
|
||||
allowNegative={false}
|
||||
decimalScale={2}
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputPrefix={'Rp'}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'transport_total').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'transport_total')
|
||||
.errorMessage
|
||||
}
|
||||
className={{
|
||||
wrapper: 'min-w-40 md:min-w-52 lg:min-w-64',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={'col-span-2'}>
|
||||
<TextInput
|
||||
label='Notes'
|
||||
name='notes'
|
||||
value={formik.values.notes || ''}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes as string}
|
||||
placeholder='Masukkan catatan'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap mt-5'>
|
||||
<div className='flex flex-row justify-end gap-2 w-full'>
|
||||
<Button
|
||||
type='button'
|
||||
color='warning'
|
||||
className='px-4'
|
||||
onClick={() => {
|
||||
formik.resetForm();
|
||||
setPurchaseOrderFormErrorMessage('');
|
||||
onCancel?.();
|
||||
onModalClose?.();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
color='primary'
|
||||
className='px-4'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={
|
||||
!formik.isValid ||
|
||||
formik.isSubmitting ||
|
||||
hasQuantityExceededErrors
|
||||
}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{purchaseOrderFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{purchaseOrderFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseOrderAcceptApprovalForm;
|
||||
@@ -0,0 +1,450 @@
|
||||
import * as Yup from 'yup';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
|
||||
type PurchaseRequestStaffApprovalFormSchemaType = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes: string | null;
|
||||
items: {
|
||||
purchase_item_id?: number;
|
||||
product_id?: number | null;
|
||||
warehouse_id?: number | null;
|
||||
product?: {
|
||||
value?: number;
|
||||
label?: string;
|
||||
} | null;
|
||||
warehouse?: {
|
||||
value?: number;
|
||||
label?: string;
|
||||
} | null;
|
||||
qty: number;
|
||||
price: number | string;
|
||||
total_price: number | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type PurchaseRequestManagerApprovalFormSchemaType = {
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
type PurchaseRequestAcceptApprovalFormSchemaType = {
|
||||
notes: string | null;
|
||||
items: {
|
||||
purchase_item?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
travel_document_path: string;
|
||||
vehicle_number: string;
|
||||
expedition_vendor?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number | string;
|
||||
transport_per_item: number | string;
|
||||
transport_total: number | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type PurchaseStaffApprovalItemSchema = {
|
||||
purchase_item_id?: number;
|
||||
product_id?: number | null;
|
||||
warehouse_id?: number | null;
|
||||
product?: {
|
||||
value?: number;
|
||||
label?: string;
|
||||
} | null;
|
||||
warehouse?: {
|
||||
value?: number;
|
||||
label?: string;
|
||||
} | null;
|
||||
qty: number;
|
||||
price: number | string;
|
||||
total_price: number | string;
|
||||
};
|
||||
|
||||
export type PurchaseAcceptApprovalItemSchema = {
|
||||
purchase_item?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
travel_document_path: string;
|
||||
vehicle_number: string;
|
||||
expedition_vendor?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number | string;
|
||||
transport_per_item: number | string;
|
||||
transport_total: number | string;
|
||||
};
|
||||
|
||||
export type PurchaseDeleteItemsSchema = {
|
||||
item_ids: number[];
|
||||
};
|
||||
|
||||
const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffApprovalItemSchema> =
|
||||
Yup.object({
|
||||
purchase_item_id: Yup.number()
|
||||
.optional()
|
||||
.min(0, 'Purchase item ID tidak valid!')
|
||||
.typeError('Purchase item ID harus berupa angka!'),
|
||||
product: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
product_id: Yup.number()
|
||||
.optional()
|
||||
.nullable()
|
||||
.typeError('Product ID harus berupa angka!'),
|
||||
warehouse: Yup.object({
|
||||
value: Yup.number(),
|
||||
label: Yup.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
warehouse_id: Yup.number()
|
||||
.optional()
|
||||
.nullable()
|
||||
.typeError('Warehouse ID harus berupa angka!'),
|
||||
qty: Yup.number()
|
||||
.required('Jumlah wajib diisi!')
|
||||
.min(1, 'Jumlah harus berupa angka lebih dari 0!')
|
||||
.typeError('Jumlah harus berupa angka lebih dari 0!'),
|
||||
price: Yup.mixed<number | string>()
|
||||
.required('Harga wajib diisi!')
|
||||
.test(
|
||||
'is-valid-price',
|
||||
'Harga harus berupa angka lebih dari atau sama dengan 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue >= 0;
|
||||
}
|
||||
)
|
||||
.typeError('Harga harus berupa angka!'),
|
||||
total_price: Yup.mixed<number | string>()
|
||||
.required('Total harga wajib diisi!')
|
||||
.test(
|
||||
'is-valid-total-price',
|
||||
'Total harga harus berupa angka lebih dari atau sama dengan 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue >= 0;
|
||||
}
|
||||
)
|
||||
.typeError('Total harga harus berupa angka!'),
|
||||
});
|
||||
|
||||
const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManagerApprovalFormSchemaType> =
|
||||
Yup.object({
|
||||
notes: Yup.string().nullable().default(null),
|
||||
});
|
||||
|
||||
const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApprovalItemSchema> =
|
||||
Yup.object({
|
||||
purchase_item: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
purchase_item_id: Yup.number()
|
||||
.min(1, 'Purchase item is required!')
|
||||
.required('Purchase item is required!')
|
||||
.typeError('Purchase item is required!')
|
||||
.test(
|
||||
'is-valid-purchase-item',
|
||||
'Purchase item must be selected!',
|
||||
function (value) {
|
||||
return Boolean(value && value > 0);
|
||||
}
|
||||
),
|
||||
received_date: Yup.string()
|
||||
.required('Tanggal penerimaan wajib diisi!')
|
||||
.typeError('Tanggal penerimaan wajib diisi!'),
|
||||
travel_number: Yup.string()
|
||||
.required('No. Surat jalan wajib diisi!')
|
||||
.typeError('No. Surat jalan wajib diisi!'),
|
||||
travel_document_path: Yup.string()
|
||||
.required('Dokumen Surat jalan wajib diisi!')
|
||||
.typeError('Dokumen Surat jalan wajib diisi!'),
|
||||
vehicle_number: Yup.string()
|
||||
.required('Nomor kendaraan wajib diisi!')
|
||||
.typeError('Nomor kendaraan wajib diisi!'),
|
||||
expedition_vendor: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
expedition_vendor_id: Yup.number()
|
||||
.min(1, 'Vendor ekspedisi wajib diisi!')
|
||||
.required('Vendor ekspedisi wajib diisi!')
|
||||
.test(
|
||||
'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>()
|
||||
.required('Jumlah diterima wajib diisi!')
|
||||
.test(
|
||||
'is-valid-received-qty',
|
||||
'Harus berupa angka lebih dari 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue > 0;
|
||||
}
|
||||
)
|
||||
.typeError('Jumlah diterima harus berupa angka!'),
|
||||
transport_per_item: Yup.mixed<string | number>()
|
||||
.required('Biaya transport per item wajib diisi!')
|
||||
.test(
|
||||
'is-valid-transport-per-item',
|
||||
'Biaya transport per item harus berupa angka lebih dari atau sama dengan 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue >= 0;
|
||||
}
|
||||
)
|
||||
.typeError('Biaya transport per item harus berupa angka!'),
|
||||
transport_total: Yup.mixed<string | number>()
|
||||
.required('Total biaya transport wajib diisi!')
|
||||
.test(
|
||||
'is-valid-transport-total',
|
||||
'Total biaya transport harus berupa angka lebih dari atau sama dengan 0!',
|
||||
function (value) {
|
||||
if (value === '' || value === null || value === undefined)
|
||||
return false;
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) : value;
|
||||
return !isNaN(numValue) && numValue >= 0;
|
||||
}
|
||||
)
|
||||
.typeError('Total biaya transport harus berupa angka!'),
|
||||
});
|
||||
|
||||
export const PurchaseRequestStaffApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestStaffApprovalFormSchemaType> =
|
||||
Yup.object({
|
||||
action: Yup.mixed<'APPROVED' | 'REJECTED'>()
|
||||
.oneOf(['APPROVED', 'REJECTED'], 'Action harus APPROVED atau REJECTED')
|
||||
.required('Action wajib diisi!')
|
||||
.default('APPROVED'),
|
||||
notes: Yup.string().nullable().default(null),
|
||||
items: Yup.array()
|
||||
.of(PurchaseStaffApprovalItemObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 item pembelian!')
|
||||
.required('Item pembelian wajib diisi!')
|
||||
.typeError('Item pembelian wajib diisi!')
|
||||
.test(
|
||||
'items-validation',
|
||||
'Setiap item harus valid: existing items hanya butuh purchase_item_id, new items butuh product_id & warehouse_id',
|
||||
function (items) {
|
||||
if (!items || items.length === 0) return false;
|
||||
|
||||
return items.every((item) => {
|
||||
const isExisting =
|
||||
item.purchase_item_id && item.purchase_item_id > 0;
|
||||
|
||||
const isNew = !item.purchase_item_id || item.purchase_item_id === 0;
|
||||
|
||||
if (isExisting) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
return Boolean(
|
||||
item.product_id &&
|
||||
item.product_id > 0 &&
|
||||
item.warehouse_id &&
|
||||
item.warehouse_id > 0
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
export const PurchaseDeleteItemsSchema: Yup.ObjectSchema<PurchaseDeleteItemsSchema> =
|
||||
Yup.object({
|
||||
item_ids: Yup.array()
|
||||
.of(
|
||||
Yup.number()
|
||||
.min(1, 'Item ID tidak valid!')
|
||||
.required('Item ID tidak valid!')
|
||||
.typeError('Item ID tidak valid!')
|
||||
)
|
||||
.min(1, 'Minimal harus ada 1 item yang dihapus!')
|
||||
.required('Item yang dihapus wajib diisi!')
|
||||
.typeError('Item yang dihapus wajib diisi!'),
|
||||
});
|
||||
|
||||
export const PurchaseRequestStaffApprovalFormInitialValues: PurchaseRequestStaffApprovalFormSchemaType =
|
||||
{
|
||||
action: 'APPROVED',
|
||||
notes: '',
|
||||
items: [
|
||||
{
|
||||
product_id: 0,
|
||||
warehouse_id: 0,
|
||||
product: null,
|
||||
warehouse: null,
|
||||
qty: 0,
|
||||
price: '',
|
||||
total_price: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const PurchaseRequestStaffApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
): PurchaseRequestStaffApprovalFormSchemaType => {
|
||||
return {
|
||||
action: 'APPROVED',
|
||||
notes: purchase?.notes ?? null,
|
||||
items: purchase?.items
|
||||
? purchase.items.map((item) => ({
|
||||
purchase_item_id: item.id,
|
||||
product_id: item.product_id || 0,
|
||||
warehouse_id: item.warehouse?.id || 0,
|
||||
product: {
|
||||
value: item.product_id || 0,
|
||||
label: item.product?.name || '',
|
||||
},
|
||||
warehouse: {
|
||||
value: item.warehouse?.id || 0,
|
||||
label: item.warehouse?.name || '',
|
||||
},
|
||||
qty: item.sub_qty || item.qty || 0,
|
||||
price: item.price,
|
||||
total_price: item.total_price,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
product_id: 0,
|
||||
warehouse_id: 0,
|
||||
product: null,
|
||||
warehouse: null,
|
||||
qty: 0,
|
||||
price: '',
|
||||
total_price: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export type PurchaseRequestStaffApprovalFormValues = Yup.InferType<
|
||||
typeof PurchaseRequestStaffApprovalFormSchema
|
||||
>;
|
||||
|
||||
export const PurchaseRequestManagerApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestManagerApprovalFormSchemaType> =
|
||||
PurchaseManagerApprovalObjectSchema;
|
||||
|
||||
export const PurchaseRequestManagerApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
): PurchaseRequestManagerApprovalFormSchemaType => {
|
||||
return {
|
||||
notes: purchase?.notes ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
export type PurchaseRequestManagerApprovalFormValues = Yup.InferType<
|
||||
typeof PurchaseRequestManagerApprovalFormSchema
|
||||
>;
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestAcceptApprovalFormSchemaType> =
|
||||
Yup.object({
|
||||
notes: Yup.string().nullable().default(null),
|
||||
items: Yup.array()
|
||||
.of(PurchaseAcceptApprovalItemObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 item pembelian!')
|
||||
.required('Item pembelian wajib diisi!')
|
||||
.typeError('Item pembelian wajib diisi!'),
|
||||
});
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||
{
|
||||
notes: '',
|
||||
items: [
|
||||
{
|
||||
purchase_item_id: 0,
|
||||
received_date: '',
|
||||
travel_number: '',
|
||||
travel_document_path: '',
|
||||
vehicle_number: '',
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
): PurchaseRequestAcceptApprovalFormSchemaType => {
|
||||
return {
|
||||
notes: purchase?.notes ?? null,
|
||||
items: purchase?.items
|
||||
? purchase.items.map((item) => ({
|
||||
purchase_item_id: item.id,
|
||||
received_date: '',
|
||||
travel_number: '',
|
||||
travel_document_path: '',
|
||||
vehicle_number: '',
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
}))
|
||||
: [
|
||||
{
|
||||
purchase_item_id: 0,
|
||||
received_date: '',
|
||||
travel_number: '',
|
||||
travel_document_path: '',
|
||||
vehicle_number: '',
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export type PurchaseRequestAcceptApprovalFormValues = Yup.InferType<
|
||||
typeof PurchaseRequestAcceptApprovalFormSchema
|
||||
>;
|
||||
|
||||
export const PurchaseDeleteItemsInitialValues: PurchaseDeleteItemsSchema = {
|
||||
item_ids: [],
|
||||
};
|
||||
|
||||
export type PurchaseDeleteItemsFormValues = Yup.InferType<
|
||||
typeof PurchaseDeleteItemsSchema
|
||||
>;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
import * as Yup from 'yup';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
|
||||
type PurchaseRequestFormSchemaType = {
|
||||
supplier?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
supplier_id: number;
|
||||
credit_term: number;
|
||||
area?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
area_id: number | undefined;
|
||||
location?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
location_id: number | undefined;
|
||||
notes: string | null;
|
||||
items: {
|
||||
warehouse?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
warehouse_id: number;
|
||||
product?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
product_id: number;
|
||||
qty: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type PurchaseItemSchema = {
|
||||
warehouse?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
warehouse_id: number;
|
||||
product?: {
|
||||
value: number;
|
||||
label: string;
|
||||
} | null;
|
||||
product_id: number;
|
||||
qty: number;
|
||||
};
|
||||
|
||||
const PurchaseItemObjectSchema: Yup.ObjectSchema<PurchaseItemSchema> =
|
||||
Yup.object({
|
||||
warehouse: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
warehouse_id: Yup.number()
|
||||
.required('Gudang wajib dipilih!')
|
||||
.min(1, 'Gudang wajib dipilih!')
|
||||
.typeError('Gudang wajib dipilih!'),
|
||||
product: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
product_id: Yup.number()
|
||||
.required('Produk wajib dipilih!')
|
||||
.min(1, 'Produk wajib dipilih!')
|
||||
.typeError('Produk wajib dipilih!'),
|
||||
qty: Yup.number()
|
||||
.required('Kuantitas wajib diisi!')
|
||||
.min(1, 'Kuantitas tidak boleh kurang dari 1!')
|
||||
.typeError('Kuantitas wajib diisi!'),
|
||||
});
|
||||
|
||||
export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSchemaType> =
|
||||
Yup.object({
|
||||
supplier: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
credit_term: Yup.number()
|
||||
.required('Jangka waktu kredit wajib diisi!')
|
||||
.min(0, 'Jangka waktu kredit tidak boleh kurang dari 0!')
|
||||
.typeError('Jangka waktu kredit wajib diisi!'),
|
||||
supplier_id: Yup.number()
|
||||
.required('Supplier wajib dipilih!')
|
||||
.min(1, 'Supplier wajib dipilih!')
|
||||
.typeError('Supplier wajib dipilih!'),
|
||||
area: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
area_id: Yup.number()
|
||||
.min(0, 'Area tidak boleh kurang dari 0!')
|
||||
.typeError('Area harus berupa angka!'),
|
||||
location: Yup.object({
|
||||
value: Yup.number().min(1).required(),
|
||||
label: Yup.string().required(),
|
||||
}).nullable(),
|
||||
location_id: Yup.number()
|
||||
.min(0, 'Lokasi tidak boleh kurang dari 0!')
|
||||
.typeError('Lokasi harus berupa angka!'),
|
||||
notes: Yup.string().nullable().default(null),
|
||||
items: Yup.array()
|
||||
.of(PurchaseItemObjectSchema)
|
||||
.min(1, 'Minimal harus ada 1 item pembelian!')
|
||||
.required('Item pembelian wajib diisi!')
|
||||
.typeError('Item pembelian wajib diisi!'),
|
||||
});
|
||||
|
||||
export const UpdatePurchaseRequestFormSchema = PurchaseRequestFormSchema;
|
||||
|
||||
export type PurchaseRequestFormValues = Yup.InferType<
|
||||
typeof PurchaseRequestFormSchema
|
||||
>;
|
||||
|
||||
export const getPurchaseRequestFormInitialValues = (
|
||||
initialValues?: Purchase
|
||||
): PurchaseRequestFormValues => ({
|
||||
supplier: initialValues?.supplier
|
||||
? {
|
||||
value: initialValues.supplier.id,
|
||||
label: initialValues.supplier.name,
|
||||
}
|
||||
: null,
|
||||
supplier_id: initialValues?.supplier?.id ?? 0,
|
||||
credit_term: initialValues?.credit_term ?? 0,
|
||||
area: initialValues?.area
|
||||
? {
|
||||
value: initialValues.area.id,
|
||||
label: initialValues.area.name,
|
||||
}
|
||||
: null,
|
||||
area_id: initialValues?.area?.id ?? undefined,
|
||||
location: initialValues?.location
|
||||
? {
|
||||
value: initialValues.location.id,
|
||||
label: initialValues.location.name,
|
||||
}
|
||||
: null,
|
||||
location_id: initialValues?.location?.id ?? undefined,
|
||||
notes: initialValues?.notes ?? null,
|
||||
items: initialValues?.items?.length
|
||||
? initialValues.items.map((item) => ({
|
||||
warehouse: item.warehouse
|
||||
? {
|
||||
value: item.warehouse.id,
|
||||
label: item.warehouse.name,
|
||||
}
|
||||
: null,
|
||||
warehouse_id: item.warehouse?.id ?? 0,
|
||||
product: item.product
|
||||
? {
|
||||
value: item.product.id,
|
||||
label: item.product.name,
|
||||
}
|
||||
: null,
|
||||
product_id: item.product?.id ?? 0,
|
||||
qty: item.qty ?? 0,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
warehouse: null,
|
||||
warehouse_id: 0,
|
||||
product: null,
|
||||
product_id: 0,
|
||||
qty: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,973 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import useSWR from 'swr';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import Button from '@/components/Button';
|
||||
import TextInput from '@/components/input/TextInput';
|
||||
import NumberInput from '@/components/input/NumberInput';
|
||||
import CheckboxInput from '@/components/input/CheckboxInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import ConfirmationModal from '@/components/modal/ConfirmationModal';
|
||||
import { useModal } from '@/components/Modal';
|
||||
|
||||
import {
|
||||
PurchaseRequestFormSchema,
|
||||
PurchaseRequestFormValues,
|
||||
getPurchaseRequestFormInitialValues,
|
||||
UpdatePurchaseRequestFormSchema,
|
||||
} from './PurchaseRequestForm.schema';
|
||||
import {
|
||||
SupplierApi,
|
||||
AreaApi,
|
||||
LocationApi,
|
||||
WarehouseApi,
|
||||
ProductApi,
|
||||
} from '@/services/api/master-data';
|
||||
import { Supplier, SupplierProducts } from '@/types/api/master-data/supplier';
|
||||
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
|
||||
import { formatNumber } from '@/lib/helper';
|
||||
import { PurchaseApi } from '@/services/api/purchase';
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import {
|
||||
CreatePurchaseRequestPayload,
|
||||
Purchase,
|
||||
} from '@/types/api/purchase/purchase';
|
||||
|
||||
interface PurchaseRequestFormProps {
|
||||
type?: 'add' | 'edit' | 'detail';
|
||||
initialValues?: Purchase;
|
||||
}
|
||||
|
||||
const PurchaseRequestForm = ({
|
||||
type = 'add',
|
||||
initialValues,
|
||||
}: PurchaseRequestFormProps) => {
|
||||
const router = useRouter();
|
||||
const deleteModal = useModal();
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const [locationSelectInputValue, setLocationSelectInputValue] = useState('');
|
||||
const [selectedPurchaseItems, setSelectedPurchaseItems] = useState<number[]>(
|
||||
[]
|
||||
);
|
||||
const [purchaseRequestFormErrorMessage, setPurchaseRequestFormErrorMessage] =
|
||||
useState('');
|
||||
|
||||
// ===== TYPE DEFINITIONS =====
|
||||
interface ProductOptionType {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const isRepeaterInputError = (
|
||||
idx: number,
|
||||
field: 'warehouse_id' | 'product_id' | 'qty'
|
||||
): { isError: boolean; errorMessage: string } => {
|
||||
if (!formik.touched.items || !Array.isArray(formik.touched.items)) {
|
||||
return {
|
||||
isError: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
}
|
||||
|
||||
const touchedField = (
|
||||
formik.touched.items[idx] as Partial<{
|
||||
warehouse_id: boolean;
|
||||
product_id: boolean;
|
||||
qty: boolean;
|
||||
}>
|
||||
)?.[field];
|
||||
const errorItem = formik.errors.items?.[idx] as
|
||||
| Record<string, string>
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
isError: Boolean(touchedField && Boolean(errorItem?.[field])),
|
||||
errorMessage: touchedField && errorItem?.[field] ? errorItem[field] : '',
|
||||
};
|
||||
};
|
||||
|
||||
// ===== SUBMISSION HANDLERS =====
|
||||
const createPurchaseRequestHandler = useCallback(
|
||||
async (payload: CreatePurchaseRequestPayload) => {
|
||||
const res = await PurchaseApi.create(payload);
|
||||
if (isResponseError(res)) {
|
||||
setPurchaseRequestFormErrorMessage(res.message);
|
||||
return;
|
||||
}
|
||||
toast.success(res?.message as string);
|
||||
router.push('/purchase');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const updatePurchaseRequestHandler = useCallback(
|
||||
async (
|
||||
purchaseRequestId: number,
|
||||
payload: CreatePurchaseRequestPayload
|
||||
) => {
|
||||
const res = await PurchaseApi.update(purchaseRequestId, payload);
|
||||
if (isResponseError(res)) {
|
||||
setPurchaseRequestFormErrorMessage(res.message);
|
||||
return;
|
||||
}
|
||||
toast.success(res?.message as string);
|
||||
router.refresh();
|
||||
router.push('/purchase');
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
const deletePurchaseRequestClickHandler = useCallback(() => {
|
||||
deleteModal.openModal();
|
||||
}, [deleteModal]);
|
||||
|
||||
const confirmationModalDeleteClickHandler = useCallback(async () => {
|
||||
if (!initialValues?.id) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
await PurchaseApi.delete(initialValues.id);
|
||||
deleteModal.closeModal();
|
||||
toast.success('Successfully delete Purchase Request!');
|
||||
setIsDeleteLoading(false);
|
||||
router.push('/purchase');
|
||||
}, [deleteModal, initialValues?.id, router]);
|
||||
|
||||
// ===== SELECT INPUT DATA =====
|
||||
const {
|
||||
setInputValue: setSupplierSelectInputValue,
|
||||
options: supplierOptions,
|
||||
isLoadingOptions: isLoadingSuppliers,
|
||||
rawData: supplierRawData,
|
||||
} = useSelect<Supplier>(SupplierApi.basePath, 'id', 'name', 'search', {
|
||||
category: 'SAPRONAK',
|
||||
});
|
||||
|
||||
const {
|
||||
setInputValue: setAreaSelectInputValue,
|
||||
options: areaOptions,
|
||||
isLoadingOptions: isLoadingAreas,
|
||||
} = useSelect(AreaApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
const {
|
||||
inputValue: warehouseSelectInputValue,
|
||||
setInputValue: setWarehouseSelectInputValue,
|
||||
isLoadingOptions: isLoadingWarehouses,
|
||||
} = useSelect(WarehouseApi.basePath, 'id', 'name', 'search');
|
||||
|
||||
// ===== FORM CONFIGURATION =====
|
||||
const formikInitialValues = useMemo<PurchaseRequestFormValues>(
|
||||
() => getPurchaseRequestFormInitialValues(initialValues),
|
||||
[initialValues]
|
||||
);
|
||||
|
||||
const formik = useFormik<PurchaseRequestFormValues>({
|
||||
initialValues: formikInitialValues,
|
||||
validationSchema:
|
||||
type === 'edit'
|
||||
? UpdatePurchaseRequestFormSchema
|
||||
: PurchaseRequestFormSchema,
|
||||
validateOnChange: true,
|
||||
validateOnBlur: true,
|
||||
validateOnMount: false,
|
||||
enableReinitialize: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload: CreatePurchaseRequestPayload = {
|
||||
supplier_id:
|
||||
typeof values.supplier_id === 'string'
|
||||
? parseInt(values.supplier_id) || 0
|
||||
: values.supplier_id || 0,
|
||||
credit_term:
|
||||
typeof values.credit_term === 'string'
|
||||
? parseInt(values.credit_term) || 0
|
||||
: values.credit_term || 0,
|
||||
notes: values.notes || '',
|
||||
items: (values.items || []).map((item) => ({
|
||||
warehouse_id: Number(item.warehouse_id) || 0,
|
||||
product_id: Number(item.product_id) || 0,
|
||||
qty: Number(item.qty) || 0,
|
||||
})),
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'add':
|
||||
await createPurchaseRequestHandler(payload);
|
||||
break;
|
||||
case 'edit':
|
||||
await updatePurchaseRequestHandler(
|
||||
initialValues?.id as number,
|
||||
payload
|
||||
);
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ===== API DATA FETCHING =====
|
||||
const { data: supplierData, isLoading: isLoadingProducts } = useSWR(
|
||||
formik.values.supplier_id && Number(formik.values.supplier_id) > 0
|
||||
? formik.values.supplier_id?.toString()
|
||||
: null,
|
||||
(id: string) => SupplierApi.getSingle(Number(id))
|
||||
);
|
||||
|
||||
const supplierProductOptions = useMemo(() => {
|
||||
if (!supplierData || !isResponseSuccess(supplierData)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const supplier = supplierData.data as SupplierProducts;
|
||||
const products = supplier.products || [];
|
||||
return products.map((product) => ({
|
||||
value: product.id,
|
||||
label: product.name,
|
||||
}));
|
||||
}, [supplierData]);
|
||||
|
||||
const supplierProductData = useMemo(() => {
|
||||
if (!supplierData || !isResponseSuccess(supplierData)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const supplier = supplierData.data as SupplierProducts;
|
||||
const products = supplier.products || [];
|
||||
const data: Record<number, NonNullable<typeof supplier.products>[0]> = {};
|
||||
|
||||
products.forEach((product) => {
|
||||
data[product.id] = product;
|
||||
});
|
||||
|
||||
return data;
|
||||
}, [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 newItems = [
|
||||
...(formik.values.items || []),
|
||||
{
|
||||
warehouse: null,
|
||||
warehouse_id: 0,
|
||||
product: null,
|
||||
product_id: 0,
|
||||
qty: 0,
|
||||
},
|
||||
];
|
||||
formik.setFieldValue('items', newItems);
|
||||
};
|
||||
|
||||
const removePurchaseItem = (idx: number) => {
|
||||
const updatedPurchaseItems = formik.values.items?.filter(
|
||||
(_, i) => i !== idx
|
||||
);
|
||||
formik.setFieldValue('items', updatedPurchaseItems);
|
||||
};
|
||||
|
||||
const removeSelectedPurchaseItems = () => {
|
||||
const updatedPurchaseItems = formik.values.items?.filter(
|
||||
(_, idx) => !selectedPurchaseItems.includes(idx)
|
||||
);
|
||||
formik.setFieldValue('items', updatedPurchaseItems);
|
||||
setSelectedPurchaseItems([]);
|
||||
};
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const updateCreditTermBasedOnSupplier = useCallback(
|
||||
(supplierId: number) => {
|
||||
if (supplierId > 0 && isResponseSuccess(supplierRawData)) {
|
||||
const supplierData = supplierRawData.data.find(
|
||||
(s: Supplier) => s.id === supplierId
|
||||
);
|
||||
if (supplierData?.due_date) {
|
||||
formik.setFieldTouched('credit_term', false);
|
||||
formik.setFieldValue('credit_term', supplierData.due_date.toString());
|
||||
} else {
|
||||
formik.setFieldTouched('credit_term', false);
|
||||
formik.setFieldValue('credit_term', '');
|
||||
}
|
||||
} else {
|
||||
formik.setFieldTouched('credit_term', false);
|
||||
formik.setFieldValue('credit_term', '');
|
||||
}
|
||||
},
|
||||
[supplierRawData]
|
||||
);
|
||||
|
||||
const resetPurchaseItems = useCallback(() => {
|
||||
if (formik.values.items) {
|
||||
formik.values.items.forEach((_, idx) => {
|
||||
formik.setFieldTouched(`items.${idx}.product`, false);
|
||||
formik.setFieldValue(`items.${idx}.product`, null);
|
||||
formik.setFieldTouched(`items.${idx}.product_id`, false);
|
||||
formik.setFieldValue(`items.${idx}.product_id`, 0);
|
||||
formik.setFieldTouched(`items.${idx}.qty`, false);
|
||||
formik.setFieldValue(`items.${idx}.qty`, 0);
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ===== SIDE EFFECTS =====
|
||||
useEffect(() => {
|
||||
if (formik.values.supplier_id && Number(formik.values.supplier_id) > 0) {
|
||||
updateCreditTermBasedOnSupplier(Number(formik.values.supplier_id));
|
||||
resetPurchaseItems();
|
||||
} else {
|
||||
formik.setFieldTouched('credit_term', false);
|
||||
formik.setFieldValue('credit_term', '');
|
||||
resetPurchaseItems();
|
||||
}
|
||||
}, [formik.values.supplier_id]);
|
||||
|
||||
// ===== FORM HANDLERS =====
|
||||
const handleSupplierChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const supplier = val as OptionType | null;
|
||||
const supplierId = Number(supplier?.value);
|
||||
|
||||
formik.setFieldTouched('supplier', true);
|
||||
formik.setFieldValue('supplier', supplier);
|
||||
formik.setFieldTouched('supplier_id', true);
|
||||
formik.setFieldValue('supplier_id', supplierId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreditTermChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
formik.setFieldTouched('credit_term', true);
|
||||
formik.setFieldValue('credit_term', value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCreditTermBlur = useCallback(
|
||||
(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
formik.handleBlur(e);
|
||||
},
|
||||
[formik]
|
||||
);
|
||||
|
||||
const handleAreaChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const area = val as OptionType | null;
|
||||
formik.setFieldTouched('area_id', true);
|
||||
formik.setFieldValue('area_id', (area as OptionType)?.value || 0);
|
||||
formik.setFieldTouched('area', true);
|
||||
formik.setFieldValue('area', area);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleLocationChange = useCallback(
|
||||
(val: OptionType | OptionType[] | null) => {
|
||||
const location = val as OptionType | null;
|
||||
formik.setFieldTouched('location_id', true);
|
||||
formik.setFieldValue('location_id', (location as OptionType)?.value || 0);
|
||||
formik.setFieldTouched('location', true);
|
||||
formik.setFieldValue('location', location);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleWarehouseChange = useCallback(
|
||||
(idx: number, val: OptionType | OptionType[] | null) => {
|
||||
const warehouse = val as OptionType | null;
|
||||
const warehouseId = (warehouse as OptionType)?.value || 0;
|
||||
|
||||
formik.setFieldTouched(`items.${idx}.warehouse`, true);
|
||||
formik.setFieldValue(`items.${idx}.warehouse`, warehouse);
|
||||
formik.setFieldTouched(`items.${idx}.warehouse_id`, true);
|
||||
formik.setFieldValue(`items.${idx}.warehouse_id`, warehouseId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// ===== PURCHASE ITEM OPERATIONS =====
|
||||
const handlePurchaseItemChange = (
|
||||
idx: number,
|
||||
field: 'qty',
|
||||
value: string | number
|
||||
) => {
|
||||
if (field === 'qty') {
|
||||
const numValue =
|
||||
typeof value === 'string' ? parseFloat(value) || 0 : value;
|
||||
formik.setFieldTouched(`items.${idx}.qty`, true);
|
||||
formik.setFieldValue(`items.${idx}.qty`, numValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className='w-full'>
|
||||
<header className='flex flex-col gap-4'>
|
||||
<Button
|
||||
href='/purchase'
|
||||
variant='link'
|
||||
className='w-fit p-0 text-primary'
|
||||
>
|
||||
<Icon icon='uil:arrow-left' width={24} height={24} />
|
||||
Kembali
|
||||
</Button>
|
||||
<h1 className='text-2xl font-bold text-center'>
|
||||
{type === 'add' && 'Tambah Purchase Request'}
|
||||
{type === 'edit' && 'Edit Purchase Request'}
|
||||
{type === 'detail' && 'Detail Purchase Request'}
|
||||
</h1>
|
||||
</header>
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
onReset={formik.handleReset}
|
||||
className='w-full mt-8 flex flex-col gap-6'
|
||||
>
|
||||
{/* Basic Info Card */}
|
||||
<Card
|
||||
title='Informasi Purchase Request'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
body: 'flex flex-col gap-6',
|
||||
}}
|
||||
>
|
||||
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
||||
<SelectInput
|
||||
required
|
||||
label='Vendor'
|
||||
placeholder='Pilih Vendor...'
|
||||
value={formik.values.supplier}
|
||||
onChange={handleSupplierChange}
|
||||
options={supplierOptions}
|
||||
onInputChange={setSupplierSelectInputValue}
|
||||
isLoading={isLoadingSuppliers}
|
||||
isError={
|
||||
formik.touched.supplier_id &&
|
||||
Boolean(formik.errors.supplier_id)
|
||||
}
|
||||
errorMessage={formik.errors.supplier_id as string}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
required={!!formik.values.supplier_id}
|
||||
label='Jatuh tempo (hari)'
|
||||
name='credit_term'
|
||||
value={formik.values.credit_term || ''}
|
||||
onChange={handleCreditTermChange}
|
||||
onBlur={handleCreditTermBlur}
|
||||
isError={
|
||||
formik.touched.credit_term &&
|
||||
Boolean(formik.errors.credit_term)
|
||||
}
|
||||
errorMessage={formik.errors.credit_term as string}
|
||||
readOnly={type === 'detail' || !formik.values.supplier_id}
|
||||
disabled={type === 'detail' || !formik.values.supplier_id}
|
||||
allowNegative={false}
|
||||
decimalScale={0}
|
||||
placeholder={
|
||||
!formik.values.supplier_id
|
||||
? 'Pilih Vendor terlebih dahulu'
|
||||
: 'Masukkan jumlah hari jatuh tempo'
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Area'
|
||||
placeholder='Pilih Area...'
|
||||
value={formik.values.area}
|
||||
onChange={handleAreaChange}
|
||||
options={areaOptions}
|
||||
onInputChange={setAreaSelectInputValue}
|
||||
isLoading={isLoadingAreas}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
label='Lokasi'
|
||||
placeholder='Pilih Lokasi...'
|
||||
value={formik.values.location}
|
||||
onChange={handleLocationChange}
|
||||
options={locationOptions}
|
||||
onInputChange={setLocationSelectInputValue}
|
||||
isLoading={isLoadingLocations}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable={type !== 'detail'}
|
||||
/>
|
||||
|
||||
<div className={'col-span-2'}>
|
||||
<TextInput
|
||||
label='Notes'
|
||||
name='notes'
|
||||
value={formik.values.notes || ''}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
isError={formik.touched.notes && Boolean(formik.errors.notes)}
|
||||
errorMessage={formik.errors.notes as string}
|
||||
readOnly={type === 'detail'}
|
||||
disabled={type === 'detail'}
|
||||
placeholder='Masukkan catatan'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Purchase Items Table */}
|
||||
<Card
|
||||
title='Item Pembelian'
|
||||
className={{
|
||||
wrapper: 'w-full mb-4 shadow',
|
||||
title: 'mb-4',
|
||||
}}
|
||||
>
|
||||
<div className='overflow-x-auto'>
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
{type !== 'detail' && (
|
||||
<th>
|
||||
<CheckboxInput
|
||||
name='select-all-items'
|
||||
checked={
|
||||
formik.values.items?.length ===
|
||||
selectedPurchaseItems.length &&
|
||||
formik.values.items?.length > 0
|
||||
}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedPurchaseItems(
|
||||
formik.values.items?.map((_, idx) => idx) ?? []
|
||||
);
|
||||
} else {
|
||||
setSelectedPurchaseItems([]);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
<th>
|
||||
Gudang
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Item
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Jumlah
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>Estimasi Harga</th>
|
||||
<th>Satuan</th>
|
||||
{type !== 'detail' && <th>Action</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.items?.map((item, idx) => (
|
||||
<tr key={`purchase-item-${idx}`}>
|
||||
{type !== 'detail' && (
|
||||
<td className='!align-middle'>
|
||||
<CheckboxInput
|
||||
name={`purchase-item-${idx}`}
|
||||
checked={selectedPurchaseItems.includes(idx)}
|
||||
onChange={(
|
||||
e: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedPurchaseItems([
|
||||
...selectedPurchaseItems,
|
||||
idx,
|
||||
]);
|
||||
} else {
|
||||
setSelectedPurchaseItems(
|
||||
selectedPurchaseItems.filter((i) => i !== idx)
|
||||
);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
wrapper: 'flex justify-center',
|
||||
checkbox: 'checkbox checkbox-sm',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td>
|
||||
<SelectInput
|
||||
placeholder='Pilih Gudang...'
|
||||
value={item.warehouse}
|
||||
onChange={(val) => handleWarehouseChange(idx, val)}
|
||||
options={warehouseOptions}
|
||||
onInputChange={setWarehouseSelectInputValue}
|
||||
isLoading={isLoadingWarehouses}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'warehouse_id').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'warehouse_id')
|
||||
.errorMessage
|
||||
}
|
||||
isDisabled={type === 'detail'}
|
||||
isClearable={type !== 'detail'}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SelectInput
|
||||
required
|
||||
value={item.product ?? undefined}
|
||||
onChange={(val) => {
|
||||
const product = val as ProductOptionType | null;
|
||||
const productId =
|
||||
(product as ProductOptionType)?.value || 0;
|
||||
|
||||
formik.setFieldTouched(
|
||||
`items.${idx}.product`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.product`,
|
||||
product
|
||||
);
|
||||
formik.setFieldTouched(
|
||||
`items.${idx}.product_id`,
|
||||
true
|
||||
);
|
||||
formik.setFieldValue(
|
||||
`items.${idx}.product_id`,
|
||||
productId
|
||||
);
|
||||
}}
|
||||
options={supplierProductOptions}
|
||||
isLoading={isLoadingProducts}
|
||||
isError={
|
||||
isRepeaterInputError(idx, 'product_id').isError
|
||||
}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'product_id').errorMessage
|
||||
}
|
||||
isDisabled={
|
||||
type === 'detail' || !formik.values.supplier_id
|
||||
}
|
||||
isClearable={
|
||||
type !== 'detail' && !!formik.values.supplier_id
|
||||
}
|
||||
placeholder={
|
||||
!formik.values.supplier_id
|
||||
? 'Pilih Vendor terlebih dahulu'
|
||||
: 'Pilih Produk'
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-52 md:min-w-72 lg:min-w-80',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
required
|
||||
name={`items.${idx}.qty`}
|
||||
value={item.qty || ''}
|
||||
onChange={(e) =>
|
||||
handlePurchaseItemChange(idx, 'qty', e.target.value)
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
placeholder={
|
||||
!formik.values.supplier_id
|
||||
? 'Pilih Vendor terlebih dahulu'
|
||||
: 'Masukkan kuantitas'
|
||||
}
|
||||
readOnly={
|
||||
type === 'detail' || !formik.values.supplier_id
|
||||
}
|
||||
disabled={
|
||||
type === 'detail' || !formik.values.supplier_id
|
||||
}
|
||||
allowNegative={false}
|
||||
decimalScale={0}
|
||||
isError={isRepeaterInputError(idx, 'qty').isError}
|
||||
errorMessage={
|
||||
isRepeaterInputError(idx, 'qty').errorMessage
|
||||
}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32 md:min-w-48 lg:min-w-52',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.price`}
|
||||
value={
|
||||
item.product_id &&
|
||||
supplierProductData[item.product_id]
|
||||
? formatNumber(
|
||||
supplierProductData[item.product_id]
|
||||
.ProductPrice *
|
||||
(parseFloat(item.qty?.toString() || '0') ||
|
||||
0)
|
||||
)
|
||||
: ''
|
||||
}
|
||||
onChange={() => {}}
|
||||
onBlur={formik.handleBlur}
|
||||
type='text'
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32 md:min-w-48 lg:min-w-52',
|
||||
}}
|
||||
disabled={true}
|
||||
readOnly={true}
|
||||
inputPrefix={'Rp'}
|
||||
placeholder={
|
||||
item.product_id
|
||||
? 'Loading...'
|
||||
: 'Pilih produk terlebih dahulu'
|
||||
}
|
||||
bottomLabel={
|
||||
item.product_id &&
|
||||
supplierProductData[item.product_id]
|
||||
? `Harga per unit: Rp ${formatNumber(
|
||||
supplierProductData[item.product_id]
|
||||
.ProductPrice
|
||||
)}`
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name={`items.${idx}.uom`}
|
||||
value={
|
||||
item.product_id &&
|
||||
supplierProductData[item.product_id]
|
||||
? supplierProductData[item.product_id].uom.name
|
||||
: ''
|
||||
}
|
||||
onBlur={formik.handleBlur}
|
||||
type='text'
|
||||
readOnly={true}
|
||||
disabled={true}
|
||||
className={{
|
||||
wrapper: 'w-full min-w-32 md:min-w-48 lg:min-w-52',
|
||||
}}
|
||||
placeholder={
|
||||
item.product_id
|
||||
? 'Loading...'
|
||||
: 'Pilih produk terlebih dahulu'
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
{type !== 'detail' && (
|
||||
<td>
|
||||
<div className='flex justify-center'>
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={() => removePurchaseItem(idx)}
|
||||
>
|
||||
<Icon
|
||||
icon='mdi:trash-can'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{type !== 'detail' && (
|
||||
<div className='flex justify-center items-center mt-4 gap-4'>
|
||||
{selectedPurchaseItems.length > 0 && (
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={removeSelectedPurchaseItems}
|
||||
disabled={selectedPurchaseItems.length === 0}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='mdi:trash-can' width={24} height={24} />
|
||||
Hapus Terpilih ({selectedPurchaseItems.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type='button'
|
||||
color='success'
|
||||
onClick={addPurchaseItem}
|
||||
className='w-fit'
|
||||
>
|
||||
<Icon icon='ic:round-plus' width={24} height={24} />
|
||||
Tambah Item
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className='flex flex-row justify-between gap-2 flex-wrap'>
|
||||
{type !== 'detail' && (
|
||||
<div className='flex flex-row justify-end gap-2 w-full'>
|
||||
<Button type='reset' color='warning' className='px-4'>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
color='primary'
|
||||
className='px-4'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{type === 'detail' && (
|
||||
<div className='flex flex-row justify-start gap-2'>
|
||||
<Button
|
||||
href={`/purchase/detail/edit/?purchaseId=${initialValues?.id}`}
|
||||
color='warning'
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:edit-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
color='error'
|
||||
onClick={deletePurchaseRequestClickHandler}
|
||||
className='px-4'
|
||||
>
|
||||
<Icon
|
||||
icon='material-symbols:delete-outline-rounded'
|
||||
width={24}
|
||||
height={24}
|
||||
className='justify-start text-sm'
|
||||
/>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{purchaseRequestFormErrorMessage && (
|
||||
<div role='alert' className='alert alert-error'>
|
||||
<Icon
|
||||
icon='material-symbols:error-outline'
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
<span>{purchaseRequestFormErrorMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{type !== 'add' && (
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
type='error'
|
||||
text='Apakah anda yakin ingin menghapus data Purchase Request ini?'
|
||||
secondaryButton={{
|
||||
text: 'Tidak',
|
||||
}}
|
||||
primaryButton={{
|
||||
text: 'Ya',
|
||||
color: 'error',
|
||||
isLoading: isDeleteLoading,
|
||||
onClick: confirmationModalDeleteClickHandler,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PurchaseRequestForm;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,534 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
Image,
|
||||
StyleSheet,
|
||||
Font,
|
||||
pdf,
|
||||
} from '@react-pdf/renderer';
|
||||
import { Icon } from '@iconify/react';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import { Purchase } from '@/types/api/purchase/purchase';
|
||||
import { formatDate, formatNumber } from '@/lib/helper';
|
||||
|
||||
Font.register({
|
||||
family: 'Helvetica',
|
||||
src: 'helvetica',
|
||||
});
|
||||
|
||||
const pdfStyles = StyleSheet.create({
|
||||
page: {
|
||||
fontSize: 10,
|
||||
fontFamily: 'Helvetica',
|
||||
padding: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
logo: {
|
||||
width: 120,
|
||||
height: 30,
|
||||
marginBottom: 8,
|
||||
},
|
||||
companyInfo: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
address: {
|
||||
fontSize: 8,
|
||||
color: '#666666',
|
||||
maxWidth: 400,
|
||||
marginBottom: 10,
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
marginBottom: 15,
|
||||
},
|
||||
titleSection: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
flex: 3,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
poInfo: {
|
||||
flex: 1,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 8,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
table: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
marginBottom: 15,
|
||||
},
|
||||
tableRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableHeader: {
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
tableCellHeader: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellHeaderLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: '#F5F5F5',
|
||||
},
|
||||
tableCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
tableBorderBottom: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
grandTotalRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#000000',
|
||||
borderTopStyle: 'solid',
|
||||
},
|
||||
grandTotalLabel: {
|
||||
flex: 3,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
},
|
||||
grandTotalValue: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
borderRightWidth: 0,
|
||||
},
|
||||
allocationSection: {
|
||||
marginBottom: 15,
|
||||
},
|
||||
allocationTable: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerTable: {
|
||||
marginTop: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
},
|
||||
innerRow: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#000000',
|
||||
borderBottomStyle: 'solid',
|
||||
},
|
||||
innerCell: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
},
|
||||
innerCellRight: {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: '#000000',
|
||||
borderRightStyle: 'solid',
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
innerCellRightLast: {
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
fontSize: 9,
|
||||
textAlign: 'right',
|
||||
},
|
||||
footer: {
|
||||
marginTop: 30,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
footerCompany: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'right',
|
||||
flex: 1,
|
||||
color: '#1f74bf',
|
||||
},
|
||||
specialInstructionTable: {
|
||||
width: '60%',
|
||||
maxWidth: 300,
|
||||
borderWidth: 1,
|
||||
borderColor: '#000000',
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
interface PurchaseOrderInvoiceProps {
|
||||
data?: Purchase;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
const [, setIsGeneratingPDF] = useState(false);
|
||||
const purchaseData = data;
|
||||
|
||||
const grandTotal = useMemo(() => {
|
||||
return (
|
||||
purchaseData?.items?.reduce(
|
||||
(sum, item) => sum + (item.total_price || 0),
|
||||
0
|
||||
) || 0
|
||||
);
|
||||
}, [purchaseData?.items]);
|
||||
|
||||
const handleDownloadPDF = async () => {
|
||||
if (!purchaseData) {
|
||||
alert('No purchase order data available');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGeneratingPDF(true);
|
||||
try {
|
||||
const PDFDocument = () => (
|
||||
<Document>
|
||||
<Page size='A4' style={pdfStyles.page}>
|
||||
{/* Header Section */}
|
||||
<View style={pdfStyles.header}>
|
||||
<Image
|
||||
src={'https://placehold.co/120x30/png'}
|
||||
style={pdfStyles.logo}
|
||||
id={'mbu-logo'}
|
||||
/>
|
||||
<Text style={pdfStyles.companyInfo}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text style={pdfStyles.address}>
|
||||
SOHO Building Lt.3 (Paris Van Java), Jalan Karang Tinggal, Kel.
|
||||
Cipedes, Kec. Sukajadi, Kota Bandung 40162
|
||||
</Text>
|
||||
<View style={pdfStyles.divider} />
|
||||
</View>
|
||||
|
||||
{/* Purchase Order Title */}
|
||||
<View style={pdfStyles.titleSection}>
|
||||
<Text style={pdfStyles.title}>PURCHASE ORDER</Text>
|
||||
<View style={pdfStyles.poInfo}>
|
||||
<Text>PO Number: {purchaseData?.po_number || '-'}</Text>
|
||||
<Text>
|
||||
Date:{' '}
|
||||
{purchaseData?.po_date
|
||||
? formatDate(purchaseData.po_date, 'DD MMM YYYY')
|
||||
: formatDate(new Date(), 'DD MMM YYYY')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Vendor and Ship To Table */}
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Vendor</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Ship To</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
{purchaseData?.supplier?.name || '-'} (
|
||||
{purchaseData?.supplier?.alias || ''})
|
||||
</Text>
|
||||
<Text>{purchaseData?.supplier?.category || '-'}</Text>
|
||||
<Text>
|
||||
Credit Term: {purchaseData?.credit_term || 0} hari
|
||||
</Text>
|
||||
<Text>
|
||||
Due Date:{' '}
|
||||
{purchaseData?.due_date
|
||||
? formatDate(purchaseData.due_date, 'DD MMM YYYY')
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text style={{ fontWeight: 'bold' }}>
|
||||
PT LUMBUNG TELUR INDONESIA
|
||||
</Text>
|
||||
<Text>
|
||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
||||
? purchaseData.items[0].warehouse.location.name
|
||||
: '-'}
|
||||
</Text>
|
||||
<Text>
|
||||
{purchaseData?.items?.[0]?.warehouse.type === 'LOKASI'
|
||||
? purchaseData.items[0].warehouse.location.address
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Item Description Table */}
|
||||
<View>
|
||||
<View style={pdfStyles.table}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Item Description</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Unit Price</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Quantity</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
{purchaseData?.items?.map((item, index) => {
|
||||
const isLastItem =
|
||||
index === (purchaseData?.items?.length || 0) - 1;
|
||||
return (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
pdfStyles.tableRow,
|
||||
isLastItem ? {} : pdfStyles.tableBorderBottom,
|
||||
]}
|
||||
>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.product?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>Rp{formatNumber(item.price || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text>{formatNumber(item.sub_qty || 0)}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRightLast}>
|
||||
<Text>Rp{formatNumber(item.total_price || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}) || []}
|
||||
|
||||
{/* Grand Total Row inside table */}
|
||||
<View style={pdfStyles.grandTotalRow}>
|
||||
<View style={[pdfStyles.tableCell, { borderRightWidth: 0 }]}>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View
|
||||
style={[pdfStyles.tableCellRight, { borderRightWidth: 0 }]}
|
||||
>
|
||||
<Text></Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellRight}>
|
||||
<Text style={{ fontWeight: 'bold' }}>Grand Total</Text>
|
||||
</View>
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.tableCellRightLast,
|
||||
{ fontWeight: 'bold' },
|
||||
]}
|
||||
>
|
||||
<Text>Rp{formatNumber(grandTotal)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Product Allocation Section */}
|
||||
<View style={pdfStyles.allocationSection}>
|
||||
<Text style={pdfStyles.sectionTitle}>Product Allocation</Text>
|
||||
<View style={pdfStyles.allocationTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Warehouse Name</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Area</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeader}>
|
||||
<Text>Location Address</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Product Allocation</Text>
|
||||
</View>
|
||||
</View>
|
||||
{purchaseData?.items?.map((item, itemIndex) => (
|
||||
<View key={itemIndex} style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.warehouse?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>{item.warehouse?.area?.name || '-'}</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCell}>
|
||||
<Text>
|
||||
{item.warehouse?.type === 'LOKASI'
|
||||
? item.warehouse.location.address
|
||||
: '-'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
{/* Inner table for product allocation */}
|
||||
<View style={pdfStyles.innerTable}>
|
||||
{/* Header for inner table */}
|
||||
<View
|
||||
style={[
|
||||
pdfStyles.innerRow,
|
||||
{ backgroundColor: '#F5F5F5' },
|
||||
]}
|
||||
>
|
||||
<Text style={pdfStyles.innerCell}>Item</Text>
|
||||
<Text style={pdfStyles.innerCellRightLast}>
|
||||
Quantity
|
||||
</Text>
|
||||
</View>
|
||||
{/* Data row */}
|
||||
<View style={pdfStyles.innerRow}>
|
||||
<Text style={pdfStyles.innerCell}>
|
||||
{item.product?.name || '-'}
|
||||
</Text>
|
||||
<Text style={pdfStyles.innerCellRightLast}>
|
||||
{formatNumber(item.total_qty || 0)} of{' '}
|
||||
{formatNumber(item.sub_qty || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)) || []}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer with Special Instructions */}
|
||||
<View style={pdfStyles.footer}>
|
||||
<View style={pdfStyles.specialInstructionTable}>
|
||||
<View style={[pdfStyles.tableRow, pdfStyles.tableHeader]}>
|
||||
<View style={pdfStyles.tableCellHeaderLast}>
|
||||
<Text>Notes</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.tableRow}>
|
||||
<View style={pdfStyles.tableCellLast}>
|
||||
<Text>{purchaseData?.notes || '-'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={pdfStyles.footerCompany}>
|
||||
<Text>PT LUMBUNG TELUR INDONESIA</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
);
|
||||
|
||||
const blob = await pdf(<PDFDocument />).toBlob();
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `${purchaseData?.po_number || 'purchase-order'}.pdf`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
} finally {
|
||||
setIsGeneratingPDF(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!purchaseData) {
|
||||
return (
|
||||
<div className='flex items-center justify-center min-h-screen'>
|
||||
<div className='text-gray-500'>No purchase order data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return purchaseData?.po_number &&
|
||||
purchaseData.po_number !== 'Belum dibuat' ? (
|
||||
<Button
|
||||
color='primary'
|
||||
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm font-mono'
|
||||
onClick={handleDownloadPDF}
|
||||
>
|
||||
<Icon icon='material-symbols:file-open-outline' width={16} height={16} />
|
||||
{purchaseData.po_number}
|
||||
</Button>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default PurchaseOrderInvoice;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '../Button';
|
||||
import Button from '@/components/Button';
|
||||
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
|
||||
|
||||
interface TableRowOptionsProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import SelectInput from '../input/SelectInput';
|
||||
import SelectInput from '@/components/input/SelectInput';
|
||||
|
||||
export interface OptionType {
|
||||
label: string;
|
||||
@@ -9,15 +9,18 @@ interface TableRowSizeSelectorProps {
|
||||
value: number;
|
||||
onChange: (val: OptionType | OptionType[] | null) => void;
|
||||
options: OptionType[];
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TableRowSizeSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
children,
|
||||
}: TableRowSizeSelectorProps) => {
|
||||
return (
|
||||
<div className='flex flex-row justify-end'>
|
||||
<div className='flex flex-row gap-3 items-end justify-end'>
|
||||
{children}
|
||||
<SelectInput
|
||||
label='Baris'
|
||||
options={options}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Icon } from '@iconify/react';
|
||||
import Button from '../Button';
|
||||
import DebouncedTextInput from '../input/DebouncedTextInput';
|
||||
import Button from '@/components/Button';
|
||||
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
|
||||
|
||||
interface TableToolbarProps {
|
||||
addButton?: {
|
||||
|
||||
@@ -93,6 +93,29 @@ export const LAYING_RECORDING_APPROVAL_LINE: ApprovalLine = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const PURCHASE_ORDER_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
step_name: 'Pengajuan',
|
||||
},
|
||||
{
|
||||
step_number: 2,
|
||||
step_name: 'Staff Purchase',
|
||||
},
|
||||
{
|
||||
step_number: 3,
|
||||
step_name: 'Manager Purchase',
|
||||
},
|
||||
{
|
||||
step_number: 4,
|
||||
step_name: 'Penerimaan Produk',
|
||||
},
|
||||
{
|
||||
step_number: 5,
|
||||
step_name: 'Selesai',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const EXPENSE_REQUEST_APPROVAL_LINE: ApprovalLine = [
|
||||
{
|
||||
step_number: 1,
|
||||
|
||||
+12
-6
@@ -41,9 +41,9 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Biaya Operasional',
|
||||
link: '/expense',
|
||||
icon: 'uil:wallet',
|
||||
title: 'Pembelian',
|
||||
link: '/purchase',
|
||||
icon: 'gg:shopping-cart',
|
||||
},
|
||||
|
||||
{
|
||||
@@ -52,6 +52,12 @@ export const MAIN_DRAWER_LINKS: MAIN_DRAWER_MENU[] = [
|
||||
icon: 'mdi:attach-money',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Biaya Operasional',
|
||||
link: '/expense',
|
||||
icon: 'uil:wallet',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Persediaan',
|
||||
link: '/inventory',
|
||||
@@ -233,9 +239,9 @@ export const SUPPLIER_FLAG_OPTIONS = [
|
||||
];
|
||||
|
||||
export const RECORDING_FLAG_OPTIONS = [
|
||||
{ label: 'Ayam Afkir', value: 'Afkir' },
|
||||
{ label: 'Ayam Culling', value: 'Culling' },
|
||||
{ label: 'Ayam Mati', value: 'Mati' },
|
||||
{ label: 'Ayam Afkir', value: 'Ayam Afkir' },
|
||||
{ label: 'Ayam Culling', value: 'Ayam Culling' },
|
||||
{ label: 'Ayam Mati', value: 'Ayam Mati' },
|
||||
];
|
||||
|
||||
export const APPROVAL_WORKFLOWS = [
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CreateChickinPayload,
|
||||
UpdateChickinPayload,
|
||||
} from '@/types/api/production/chickin';
|
||||
import { BaseApiService } from '../base';
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { httpClient } from '@/services/http/client';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ProjectFlock,
|
||||
UpdateProjectFlockPayload,
|
||||
} from '@/types/api/production/project-flock';
|
||||
import { BaseApiService } from '../base';
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import {
|
||||
BaseApiResponse,
|
||||
BaseGroupedApproval,
|
||||
@@ -120,7 +120,7 @@ export class ProjectFlockService extends BaseApiService<
|
||||
| undefined
|
||||
> {
|
||||
try {
|
||||
const path = `${this.basePath}/location/${locationId.toString()}/periods`;
|
||||
const path = `${this.basePath}/locations/${locationId.toString()}/periods`;
|
||||
return await httpClient<
|
||||
SuccessApiResponse<
|
||||
{
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
CreatePurchaseRequestPayload,
|
||||
Purchase,
|
||||
UpdatePurchaseRequestPayload,
|
||||
CreateStaffApprovalRequestPayload,
|
||||
UpdateStaffApprovalRequestPayload,
|
||||
CreateManagerApprovalRequestPayload,
|
||||
CreateAcceptApprovalRequestPayload,
|
||||
DeletePurchaseRequestItemPayload,
|
||||
} from '@/types/api/purchase/purchase';
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
|
||||
const basePurchaseApi = new BaseApiService<
|
||||
Purchase,
|
||||
CreatePurchaseRequestPayload,
|
||||
UpdatePurchaseRequestPayload
|
||||
>('/purchases');
|
||||
|
||||
export const PurchaseApi = {
|
||||
basePath: basePurchaseApi.basePath,
|
||||
header: basePurchaseApi.header,
|
||||
getAllFetcher: basePurchaseApi.getAllFetcher.bind(basePurchaseApi),
|
||||
getSingle: basePurchaseApi.getSingle.bind(basePurchaseApi),
|
||||
create: basePurchaseApi.create.bind(basePurchaseApi),
|
||||
update: basePurchaseApi.update.bind(basePurchaseApi),
|
||||
delete: basePurchaseApi.delete.bind(basePurchaseApi),
|
||||
customRequest: basePurchaseApi.customRequest.bind(basePurchaseApi),
|
||||
|
||||
staffApproval: {
|
||||
create: async (
|
||||
purchaseRequestId: number,
|
||||
payload: CreateStaffApprovalRequestPayload
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||
return await basePurchaseApi.customRequest<
|
||||
BaseApiResponse<{ message: string }>
|
||||
>(`${purchaseRequestId}/approvals/staff`, {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
|
||||
update: async (
|
||||
purchaseRequestId: number,
|
||||
payload: UpdateStaffApprovalRequestPayload
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||
return await basePurchaseApi.customRequest<
|
||||
BaseApiResponse<{ message: string }>
|
||||
>(`${purchaseRequestId}/approvals/staff`, {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
managerApproval: {
|
||||
create: async (
|
||||
purchaseRequestId: number,
|
||||
payload: CreateManagerApprovalRequestPayload
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||
return await basePurchaseApi.customRequest<
|
||||
BaseApiResponse<{ message: string }>
|
||||
>(`${purchaseRequestId}/approvals/manager`, {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
acceptApproval: {
|
||||
create: async (
|
||||
purchaseRequestId: number,
|
||||
payload: CreateAcceptApprovalRequestPayload
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||
return await basePurchaseApi.customRequest<
|
||||
BaseApiResponse<{ message: string }>
|
||||
>(`${purchaseRequestId}/receipts`, {
|
||||
method: 'POST',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
items: {
|
||||
delete: async (
|
||||
purchaseRequestId: number,
|
||||
payload: DeletePurchaseRequestItemPayload
|
||||
): Promise<BaseApiResponse<{ message: string }> | undefined> => {
|
||||
return await basePurchaseApi.customRequest<
|
||||
BaseApiResponse<{ message: string }>
|
||||
>(`${purchaseRequestId}/items`, {
|
||||
method: 'DELETE',
|
||||
payload,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { BaseMetadata } from '../base-metadata';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
|
||||
export type BaseInventoryAdjustment = {
|
||||
id: number;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Kandang } from '@/types/api/master-data/kandang';
|
||||
import { id } from 'react-day-picker/locale';
|
||||
import { Warehouse } from '../master-data/warehouse';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
|
||||
/**
|
||||
* Base Data Response
|
||||
|
||||
+12
@@ -1,4 +1,5 @@
|
||||
import { BaseMetadata } from '@/types/api/api-general';
|
||||
import { Uom } from '@/types/api/master-data/uom';
|
||||
|
||||
export type BaseSupplier = {
|
||||
id: number;
|
||||
@@ -19,6 +20,17 @@ export type BaseSupplier = {
|
||||
|
||||
export type Supplier = BaseMetadata & BaseSupplier;
|
||||
|
||||
export type SupplierProducts = Supplier & {
|
||||
products?: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
ProductPrice: number;
|
||||
SellingPrice?: number;
|
||||
uom: Uom;
|
||||
flags: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export type CreateSupplierPayload = {
|
||||
name: string;
|
||||
alias: string;
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { Kandang } from '@/type/master-data/kandang';
|
||||
import { ProjectFlock } from '@/types/api/production/project-flock';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Supplier } from '../master-data/supplier';
|
||||
import { BaseApproval } from '../api-general';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { BaseApproval } from '@/types/api/api-general';
|
||||
|
||||
export type BaseProjectFlockKandang = {
|
||||
id: number;
|
||||
|
||||
Vendored
+128
@@ -0,0 +1,128 @@
|
||||
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
|
||||
import { Supplier } from '@/types/api/master-data/supplier';
|
||||
import { Warehouse } from '@/types/api/master-data/warehouse';
|
||||
import { Product } from '@/types/api/master-data/product';
|
||||
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
|
||||
import { Area } from '@/types/api/master-data/area';
|
||||
import { Location } from '@/types/api/master-data/location';
|
||||
|
||||
export type PurchaseItemProduct = {
|
||||
id: number;
|
||||
name: string;
|
||||
flags?: string[];
|
||||
uom?: {
|
||||
name: string;
|
||||
};
|
||||
product_category?:
|
||||
| {
|
||||
name: string;
|
||||
}
|
||||
| string;
|
||||
};
|
||||
|
||||
export type PurchaseItem = {
|
||||
id: number;
|
||||
product_id: number;
|
||||
warehouse: Warehouse;
|
||||
product: PurchaseItemProduct | Product;
|
||||
product_warehouse: ProductWarehouse;
|
||||
quantity: number;
|
||||
qty: number;
|
||||
sub_qty: number;
|
||||
total_qty: number;
|
||||
total_used: number;
|
||||
price: number;
|
||||
total_price: number;
|
||||
received_date?: string | null;
|
||||
travel_number?: string | null;
|
||||
travel_number_docs?: string | null;
|
||||
travel_document_path?: string | null;
|
||||
vehicle_number?: string | null;
|
||||
expedition_vendor_id?: number | null;
|
||||
expedition_vendor_name?: string | null;
|
||||
received_qty?: number | null;
|
||||
transport_per_item?: number | null;
|
||||
transport_total?: number | null;
|
||||
};
|
||||
|
||||
export type BasePurchase = {
|
||||
id: number;
|
||||
pr_number: string;
|
||||
po_number: string;
|
||||
po_document_path?: string | null;
|
||||
po_date: string;
|
||||
supplier: Supplier;
|
||||
credit_term: number;
|
||||
due_date: string;
|
||||
grand_total: number;
|
||||
notes?: string | null;
|
||||
deleted_at?: string | null;
|
||||
created_by: number;
|
||||
area?: Area;
|
||||
location?: Location;
|
||||
warehouse?: Warehouse;
|
||||
items?: PurchaseItem[];
|
||||
approval?: BaseApproval;
|
||||
};
|
||||
|
||||
export type Purchase = BaseMetadata & BasePurchase;
|
||||
|
||||
export type CreatePurchaseRequestPayload = {
|
||||
supplier_id: number;
|
||||
credit_term: number;
|
||||
notes?: string | null;
|
||||
items: {
|
||||
warehouse_id: number;
|
||||
product_id: number;
|
||||
qty: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type CreateStaffApprovalRequestPayload = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes?: string | null;
|
||||
items: {
|
||||
purchase_item_id: number;
|
||||
qty: number;
|
||||
price: number;
|
||||
total_price: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateStaffApprovalRequestPayload = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes?: string | null;
|
||||
items: Array<{
|
||||
purchase_item_id?: number;
|
||||
product_id?: number;
|
||||
warehouse_id?: number;
|
||||
qty: number;
|
||||
price: number;
|
||||
total_price: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type CreateManagerApprovalRequestPayload = {
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export type CreateAcceptApprovalRequestPayload = {
|
||||
notes?: string;
|
||||
items: {
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
travel_number: string;
|
||||
travel_document_path: string;
|
||||
vehicle_number: string;
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number;
|
||||
transport_per_item: number;
|
||||
transport_total: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type DeletePurchaseRequestItemPayload = {
|
||||
item_ids: number[];
|
||||
};
|
||||
|
||||
export type UpdatePurchaseRequestPayload = CreatePurchaseRequestPayload;
|
||||
Reference in New Issue
Block a user