feat(FE-208,212): add PurchaseOrderAcceptApprovalForm and validation schema for acceptance of purchase requisitions

This commit is contained in:
rstubryan
2025-11-11 14:06:53 +07:00
parent 21ac73527d
commit 8c17367fb6
2 changed files with 1110 additions and 5 deletions
@@ -0,0 +1,872 @@
'use client';
import { useCallback, 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 Link from 'next/link';
import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import {
PurchaseRequisitionsAcceptApprovalFormDefaultValues,
PurchaseRequisitionsAcceptApprovalFormInitialValues,
PurchaseRequisitionsAcceptApprovalFormSchema,
} from './PurchaseOrderForm.schema';
import { isResponseError } from '@/lib/api-helper';
import { AcceptApprovalApi } from '@/services/api/purchase';
import {
CreateAcceptApprovalRequisitionsPayload,
Purchase,
} from '@/types/api/purchase/purchase';
import Card from '@/components/Card';
interface PurchaseOrderAcceptApprovalFormProps {
type?: 'add' | 'edit';
initialValues?: Purchase;
}
const PurchaseOrderAcceptApprovalForm = ({
type = 'add',
initialValues,
}: PurchaseOrderAcceptApprovalFormProps) => {
const searchParams = useSearchParams();
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
useState('');
// ===== TYPE DEFINITIONS =====
interface PurchaseItemOptionType extends OptionType {
id: number;
quantity: number;
product: {
name: string;
product_category: string | { name: string };
uom: {
name: string;
};
};
warehouse: {
name: string;
};
}
interface WarehouseOptionType {
value: number;
label: string;
id: number;
}
interface ExpeditionVendorOptionType {
value: number;
label: string;
id: number;
}
// ===== UTILITY FUNCTIONS =====
const getPurchaseItemError = (
idx: number,
field:
| 'purchase_item_id'
| 'received_date'
| 'warehouse_id'
| '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: CreateAcceptApprovalRequisitionsPayload) => {
const purchaseRequisitionId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: initialValues?.id || 1;
if (!purchaseRequisitionId) {
setPurchaseOrderFormErrorMessage('Purchase Requisition ID is required');
return;
}
const res = await AcceptApprovalApi.acceptApproval(
purchaseRequisitionId,
payload
);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
},
[initialValues?.id, searchParams]
);
const updateAcceptApprovalHandler = useCallback(
async (
purchaseId: number,
payload: CreateAcceptApprovalRequisitionsPayload
) => {
const res = await AcceptApprovalApi.acceptApproval(purchaseId, payload);
if (isResponseError(res)) {
setPurchaseOrderFormErrorMessage(res.message);
return;
}
toast.success(res?.message as string);
window.location.href = '/purchase';
},
[]
);
// ===== FORM CONFIGURATION =====
const formikInitialValues = useMemo(() => {
return initialValues
? PurchaseRequisitionsAcceptApprovalFormDefaultValues(initialValues)
: PurchaseRequisitionsAcceptApprovalFormInitialValues;
}, [initialValues]);
const formik = useFormik({
initialValues: formikInitialValues,
validationSchema: PurchaseRequisitionsAcceptApprovalFormSchema,
validateOnChange: true,
validateOnBlur: true,
onSubmit: async (values) => {
const payload: CreateAcceptApprovalRequisitionsPayload = {
notes: values.notes || '',
items: (values.items || []).map((item) => ({
purchase_item_id:
typeof item.purchase_item_id === 'string'
? parseInt(item.purchase_item_id) || 0
: item.purchase_item_id || 0,
received_date: item.received_date || '',
warehouse_id:
typeof item.warehouse_id === 'string'
? parseInt(item.warehouse_id) || 0
: item.warehouse_id || 0,
travel_number: item.travel_number || '',
travel_document_path: item.travel_document_path || '',
vehicle_number: item.vehicle_number || '',
expedition_vendor_id:
typeof item.expedition_vendor_id === 'string'
? parseInt(item.expedition_vendor_id) || 0
: item.expedition_vendor_id || 0,
received_qty:
typeof item.received_qty === 'string'
? parseFloat(item.received_qty) || 0
: item.received_qty || 0,
transport_per_item:
typeof item.transport_per_item === 'string'
? parseFloat(item.transport_per_item) || 0
: item.transport_per_item || 0,
transport_total:
typeof item.transport_total === 'string'
? parseFloat(item.transport_total) || 0
: item.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} (${item.quantity} ${item.product.uom.name})`,
id: item.id,
quantity: item.quantity,
product: {
name: item.product.name,
product_category: item.product.product_category,
uom: {
name: item.product.uom.name,
},
},
warehouse: {
name: item.warehouse?.name || '',
},
}));
}
return [
{
value: 1,
label: 'SEALYTE SPARK 1 x 87 gr (14 SACHET)',
id: 1,
quantity: 14,
product: {
name: 'SEALYTE SPARK 1 x 87 gr',
product_category: 'Bahan Baku',
uom: {
name: 'SACHET',
},
},
warehouse: {
name: 'GUDANG CIANGSANA 1 (ARCA P15)',
},
},
{
value: 2,
label: 'CID-2000 @ 5 KG (2 KILOGRAM)',
id: 2,
quantity: 2,
product: {
name: 'CID-2000 @ 5 KG',
product_category: 'Bahan Baku',
uom: {
name: 'Kilogram',
},
},
warehouse: {
name: 'GUDANG CIANGSANA 2 (ARCA P15)',
},
},
{
value: 3,
label: 'VITAMIN AYAM (10 DOSIS)',
id: 3,
quantity: 10,
product: {
name: 'VITAMIN AYAM',
product_category: 'Bahan Baku',
uom: {
name: 'DOSIS',
},
},
warehouse: {
name: 'GUDANG CIANGSANA 3 (ARCA P15)',
},
},
];
}, [initialValues?.items, searchParams]);
const warehouses: WarehouseOptionType[] = useMemo(
() => [
{ value: 1, label: 'GUDANG CIANGSANA 1 (ARCA P15)', id: 1 },
{ value: 2, label: 'GUDANG CIANGSANA 2 (ARCA P15)', id: 2 },
{ value: 3, label: 'GUDANG CIANGSANA 3 (ARCA P15)', id: 3 },
{ value: 7, label: 'GUDANG UTAMA', id: 7 },
],
[]
);
const expeditionVendors: ExpeditionVendorOptionType[] = useMemo(
() => [
{ value: 1, label: 'JNE Express', id: 1 },
{ value: 2, label: 'J&T Express', id: 2 },
{ value: 3, label: 'SiCepat Express', id: 3 },
{ value: 4, label: 'Anteraja', id: 4 },
],
[]
);
const getPurchaseItemOptions = useCallback(() => {
return purchaseItems;
}, [purchaseItems]);
const getWarehouseOptions = useCallback(() => {
return warehouses;
}, [warehouses]);
const getExpeditionVendorOptions = useCallback(() => {
return expeditionVendors;
}, [expeditionVendors]);
// ===== FIELD CHANGE HANDLERS =====
const purchaseItemChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
) => {
const purchaseItem = val as PurchaseItemOptionType | null;
formik.setFieldTouched(`items.${idx}.purchase_item`, true);
formik.setFieldValue(`items.${idx}.purchase_item`, purchaseItem);
formik.setFieldTouched(`items.${idx}.purchase_item_id`, true);
formik.setFieldValue(
`items.${idx}.purchase_item_id`,
(purchaseItem as OptionType)?.value || 0
);
};
const warehouseChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
) => {
const warehouse = val as WarehouseOptionType | null;
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`,
warehouse?.value || 0
);
};
const expeditionVendorChangeHandler = (
idx: number,
val: OptionType | OptionType[] | null
) => {
const expeditionVendor = val as ExpeditionVendorOptionType | 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 (
<>
<section className='w-full'>
<form
onSubmit={formik.handleSubmit}
className='w-full mt-8 flex flex-col gap-6'
>
<Card
title='Konfirmasi Penerimaan Produk'
className={{
wrapper: 'w-full mb-4 shadow',
title: 'mb-4',
}}
>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>
Item
<span className='text-error'>*</span>
</th>
<th>Gudang</th>
<th>Produk</th>
<th>Jenis Produk</th>
<th>Jumlah</th>
<th>Satuan</th>
<th>
Tanggal Diterima
<span className='text-error'>*</span>
</th>
<th>
Gudang Tujuan
<span className='text-error'>*</span>
</th>
<th>
Nomor Perjalanan
<span className='text-error'>*</span>
</th>
<th>
Dokumen Perjalanan
<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>
{formik.values.items?.map((item, idx) => {
const selectedPurchaseItem = purchaseItems.find(
(p) => p.value === item.purchase_item_id
);
return (
<tr key={`purchase-item-${idx}`}>
<td>
<SelectInput
required
isClearable={true}
value={item.purchase_item}
key={`purchase-item-${idx}`}
onChange={(val) =>
purchaseItemChangeHandler(idx, val)
}
options={getPurchaseItemOptions()}
isError={
getPurchaseItemError(idx, 'purchase_item_id')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'purchase_item_id')
.errorMessage
}
placeholder='Pilih Item...'
className={{
wrapper: 'min-w-48',
}}
/>
</td>
<td>
<TextInput
name={`items.${idx}.warehouse`}
type='text'
value={selectedPurchaseItem?.warehouse?.name || ''}
readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{
wrapper: 'min-w-32',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.product_name`}
type='text'
value={selectedPurchaseItem?.product?.name || ''}
readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{
wrapper: 'min-w-48',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.product_category`}
type='text'
value={
typeof selectedPurchaseItem?.product
?.product_category === 'string'
? selectedPurchaseItem.product.product_category
: selectedPurchaseItem?.product
?.product_category?.name || ''
}
readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{
wrapper: 'min-w-32',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.quantity`}
type='text'
value={
selectedPurchaseItem?.quantity
? selectedPurchaseItem.quantity.toLocaleString(
'id-ID'
)
: ''
}
readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<TextInput
name={`items.${idx}.uom`}
type='text'
value={
selectedPurchaseItem?.product?.uom?.name || ''
}
readOnly={true}
placeholder='Pilih item terlebih dahulu'
className={{
wrapper: 'min-w-24',
}}
disabled={true}
/>
</td>
<td>
<TextInput
required
name={`items.${idx}.received_date`}
type='date'
value={item.received_date || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.received_date`,
e.target.value
)
}
onBlur={formik.handleBlur}
isError={
getPurchaseItemError(idx, 'received_date').isError
}
errorMessage={
getPurchaseItemError(idx, 'received_date')
.errorMessage
}
className={{
wrapper: 'min-w-36',
}}
/>
</td>
<td>
<SelectInput
required
isClearable={true}
value={item.warehouse}
key={`warehouse-${idx}`}
onChange={(val) => warehouseChangeHandler(idx, val)}
options={getWarehouseOptions()}
isError={
getPurchaseItemError(idx, 'warehouse_id').isError
}
errorMessage={
getPurchaseItemError(idx, 'warehouse_id')
.errorMessage
}
placeholder='Pilih Gudang...'
className={{
wrapper: 'min-w-48',
}}
/>
</td>
<td>
<TextInput
required
name={`items.${idx}.travel_number`}
type='text'
value={item.travel_number || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.travel_number`,
e.target.value
)
}
onBlur={formik.handleBlur}
isError={
getPurchaseItemError(idx, 'travel_number').isError
}
errorMessage={
getPurchaseItemError(idx, 'travel_number')
.errorMessage
}
placeholder='Masukkan nomor perjalanan'
className={{
wrapper: 'min-w-32',
}}
/>
</td>
<td>
<TextInput
required
name={`items.${idx}.travel_document_path`}
type='text'
value={item.travel_document_path || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.travel_document_path`,
e.target.value
)
}
onBlur={formik.handleBlur}
isError={
getPurchaseItemError(idx, 'travel_document_path')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'travel_document_path')
.errorMessage
}
placeholder='Masukkan path dokumen'
className={{
wrapper: 'min-w-48',
}}
/>
</td>
<td>
<TextInput
required
name={`items.${idx}.vehicle_number`}
type='text'
value={item.vehicle_number || ''}
onChange={(e) =>
formik.setFieldValue(
`items.${idx}.vehicle_number`,
e.target.value
)
}
onBlur={formik.handleBlur}
isError={
getPurchaseItemError(idx, 'vehicle_number')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'vehicle_number')
.errorMessage
}
placeholder='Masukkan nomor kendaraan'
className={{
wrapper: 'min-w-32',
}}
/>
</td>
<td>
<SelectInput
required
isClearable={true}
value={item.expedition_vendor}
key={`expedition-vendor-${idx}`}
onChange={(val) =>
expeditionVendorChangeHandler(idx, val)
}
options={getExpeditionVendorOptions()}
isError={
getPurchaseItemError(idx, 'expedition_vendor_id')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'expedition_vendor_id')
.errorMessage
}
placeholder='Pilih Vendor...'
className={{
wrapper: 'min-w-40',
}}
/>
</td>
<td>
<NumberInput
required
name={`items.${idx}.received_qty`}
value={item.received_qty || ''}
onChange={(e) =>
handlePurchaseItemChange(
idx,
'received_qty',
e.target.value
)
}
onBlur={formik.handleBlur}
placeholder='Masukkan jumlah diterima'
allowNegative={false}
decimalScale={0}
thousandSeparator=','
decimalSeparator='.'
isError={
getPurchaseItemError(idx, 'received_qty').isError
}
errorMessage={
getPurchaseItemError(idx, 'received_qty')
.errorMessage
}
className={{
wrapper: 'min-w-32',
}}
/>
</td>
<td>
<NumberInput
required
name={`items.${idx}.transport_per_item`}
value={item.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={
getPurchaseItemError(idx, 'transport_per_item')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'transport_per_item')
.errorMessage
}
className={{
wrapper: 'min-w-32',
}}
/>
</td>
<td>
<NumberInput
required
name={`items.${idx}.transport_total`}
value={item.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={
getPurchaseItemError(idx, 'transport_total')
.isError
}
errorMessage={
getPurchaseItemError(idx, 'transport_total')
.errorMessage
}
className={{
wrapper: 'min-w-32',
}}
/>
</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'>
<Link href='/purchase'>
<Button color='warning' className='px-4'>
Cancel
</Button>
</Link>
<Button
type='submit'
color='primary'
className='px-4'
isLoading={formik.isSubmitting}
disabled={!formik.isValid || formik.isSubmitting}
>
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>
)}
</Card>
</form>
</section>
</>
);
};
export default PurchaseOrderAcceptApprovalForm;
@@ -14,6 +14,34 @@ type PurchaseRequisitionsStaffApprovalFormSchemaType = {
}[];
};
type PurchaseRequisitionsAcceptApprovalFormSchemaType = {
notes: string | null;
items: {
purchase_item?: {
value: number;
label: string;
} | null;
purchase_item_id: number;
received_date: string;
warehouse?: {
value: number;
label: string;
} | null;
warehouse_id: number;
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?: {
value: number;
@@ -24,6 +52,31 @@ export type PurchaseStaffApprovalItemSchema = {
total_price: number | string;
};
export type PurchaseAcceptApprovalItemSchema = {
purchase_item?: {
value: number;
label: string;
} | null;
purchase_item_id: number;
received_date: string;
warehouse?: {
value: number;
label: string;
} | null;
warehouse_id: number;
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;
};
const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffApprovalItemSchema> =
Yup.object({
purchase_item: Yup.object({
@@ -31,11 +84,16 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
label: Yup.string().required(),
}).nullable(),
purchase_item_id: Yup.number()
.min(1, 'Purchase item is required!')
.required('Purchase item is required!')
.test('is-valid-purchase-item', 'Purchase item must be selected!', function (value) {
if (!this.parent.purchase_item) return true;
return Boolean(value && value > 0);
})
.test(
'is-valid-purchase-item',
'Purchase item must be selected!',
function (value) {
if (!this.parent.purchase_item) return true;
return Boolean(value && value > 0);
}
)
.typeError('Purchase item must be selected!'),
price: Yup.mixed<string | number>()
.required('Harga wajib diisi!')
@@ -49,7 +107,8 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
typeof value === 'string' ? parseFloat(value) : value;
return !isNaN(numValue) && numValue >= 0;
}
),
)
.typeError('Harga harus berupa angka lebih dari atau sama dengan 0!'),
total_price: Yup.mixed<string | number>()
.required('Total harga wajib diisi!')
.test(
@@ -65,6 +124,112 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
),
});
const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApprovalItemSchema> =
Yup.object({
purchase_item: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
purchase_item_id: Yup.number()
.min(1, 'Purchase item is required!')
.required('Purchase item is required!')
.test(
'is-valid-purchase-item',
'Purchase item must be selected!',
function (value) {
if (!this.parent.purchase_item) return true;
return Boolean(value && value > 0);
}
)
.typeError('Purchase item must be selected!'),
received_date: Yup.string()
.required('Tanggal penerimaan wajib diisi!')
.typeError('Tanggal penerimaan wajib diisi!'),
warehouse: Yup.object({
value: Yup.number().min(1).required(),
label: Yup.string().required(),
}).nullable(),
warehouse_id: Yup.number()
.min(1, 'Gudang tujuan wajib diisi!')
.required('Gudang tujuan wajib diisi!')
.test(
'is-valid-warehouse',
'Gudang tujuan harus dipilih!',
function (value) {
if (!this.parent.warehouse) return true;
return Boolean(value && value > 0);
}
)
.typeError('Gudang tujuan harus dipilih!'),
travel_number: Yup.string()
.required('Nomor perjalanan wajib diisi!')
.typeError('Nomor perjalanan wajib diisi!'),
travel_document_path: Yup.string()
.required('Dokumen perjalanan wajib diisi!')
.typeError('Dokumen perjalanan 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',
'Jumlah diterima 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 PurchaseRequisitionsStaffApprovalFormSchema: Yup.ObjectSchema<PurchaseRequisitionsStaffApprovalFormSchemaType> =
Yup.object({
notes: Yup.string().nullable().default(null),
@@ -111,3 +276,71 @@ export const PurchaseRequisitionsStaffApprovalFormDefaultValues = (
export type PurchaseRequisitionsStaffApprovalFormValues = Yup.InferType<
typeof PurchaseRequisitionsStaffApprovalFormSchema
>;
export const PurchaseRequisitionsAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseRequisitionsAcceptApprovalFormSchemaType> =
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 PurchaseRequisitionsAcceptApprovalFormInitialValues: PurchaseRequisitionsAcceptApprovalFormSchemaType =
{
notes: '',
items: [
{
purchase_item_id: 0,
received_date: '',
warehouse_id: 0,
travel_number: '',
travel_document_path: '',
vehicle_number: '',
expedition_vendor_id: 0,
received_qty: '',
transport_per_item: '',
transport_total: '',
},
],
};
export const PurchaseRequisitionsAcceptApprovalFormDefaultValues = (
purchase?: Purchase
): PurchaseRequisitionsAcceptApprovalFormSchemaType => {
return {
notes: purchase?.notes ?? null,
items: purchase?.items
? purchase.items.map((item) => ({
purchase_item_id: item.id,
received_date: '',
warehouse_id: 0,
travel_number: '',
travel_document_path: '',
vehicle_number: '',
expedition_vendor_id: 0,
received_qty: '',
transport_per_item: '',
transport_total: '',
}))
: [
{
purchase_item_id: 0,
received_date: '',
warehouse_id: 0,
travel_number: '',
travel_document_path: '',
vehicle_number: '',
expedition_vendor_id: 0,
received_qty: '',
transport_per_item: '',
transport_total: '',
},
],
};
};
export type PurchaseRequisitionsAcceptApprovalFormValues = Yup.InferType<
typeof PurchaseRequisitionsAcceptApprovalFormSchema
>;