mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
Merge branch 'feat/FE/US-278/purchase-and-expense' into 'development'
[FEAT/FE][US#278] Adjust Purchase Request and Purchase Order (Expense Extended) See merge request mbugroup/lti-web-client!86
This commit is contained in:
@@ -16,7 +16,7 @@ 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 { cn, formatDate } from '@/lib/helper';
|
||||
import { isResponseSuccess } from '@/lib/api-helper';
|
||||
|
||||
import { useTableFilter } from '@/services/hooks/useTableFilter';
|
||||
@@ -136,14 +136,6 @@ const PurchaseTable = () => {
|
||||
? 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) => {
|
||||
@@ -156,11 +148,6 @@ const PurchaseTable = () => {
|
||||
return `${diffDays} hari`;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'grand_total',
|
||||
header: 'Total (Rp.)',
|
||||
cell: (props) => formatCurrency(props.row.original.grand_total),
|
||||
},
|
||||
{
|
||||
header: 'Aksi',
|
||||
cell: (props) => {
|
||||
|
||||
@@ -52,6 +52,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
const [purchaseOrderFormErrorMessage, setPurchaseOrderFormErrorMessage] =
|
||||
useState('');
|
||||
|
||||
const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const isRepeaterInputError = (
|
||||
idx: number,
|
||||
@@ -64,7 +66,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
| '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
|
||||
@@ -163,6 +164,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
validateOnBlur: true,
|
||||
onSubmit: async (values) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
notes: values.notes || '',
|
||||
items:
|
||||
values.items?.map((formItem) => {
|
||||
@@ -181,10 +183,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
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,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
@@ -239,9 +237,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
vehicle_number: item.vehicle_number || '',
|
||||
expedition_vendor: null,
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
received_qty: item.total_qty || '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
};
|
||||
});
|
||||
formik.setFieldValue('items', updatedItems);
|
||||
@@ -301,7 +298,7 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
// ===== PURCHASE ITEM OPERATIONS =====
|
||||
const handlePurchaseItemChange = (
|
||||
idx: number,
|
||||
field: 'received_qty' | 'transport_per_item' | 'transport_total',
|
||||
field: 'received_qty' | 'transport_per_item',
|
||||
value: string | number
|
||||
) => {
|
||||
const numValue = typeof value === 'string' ? parseFloat(value) || 0 : value;
|
||||
@@ -318,26 +315,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
: 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
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -386,10 +363,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
Transport/Item
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
<th>
|
||||
Total Transport
|
||||
<span className='text-error'>*</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -657,37 +630,6 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@@ -732,7 +674,8 @@ const PurchaseOrderAcceptApprovalForm = ({
|
||||
disabled={
|
||||
!formik.isValid ||
|
||||
formik.isSubmitting ||
|
||||
hasQuantityExceededErrors
|
||||
hasQuantityExceededErrors ||
|
||||
isRejected
|
||||
}
|
||||
>
|
||||
Submit
|
||||
|
||||
@@ -23,10 +23,12 @@ type PurchaseRequestStaffApprovalFormSchemaType = {
|
||||
};
|
||||
|
||||
type PurchaseRequestManagerApprovalFormSchemaType = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes: string | null;
|
||||
};
|
||||
|
||||
type PurchaseRequestAcceptApprovalFormSchemaType = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes: string | null;
|
||||
items: {
|
||||
purchase_item?: {
|
||||
@@ -45,7 +47,6 @@ type PurchaseRequestAcceptApprovalFormSchemaType = {
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number | string;
|
||||
transport_per_item: number | string;
|
||||
transport_total: number | string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -83,7 +84,6 @@ export type PurchaseAcceptApprovalItemSchema = {
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number | string;
|
||||
transport_per_item: number | string;
|
||||
transport_total: number | string;
|
||||
};
|
||||
|
||||
export type PurchaseDeleteItemsSchema = {
|
||||
@@ -152,6 +152,10 @@ const PurchaseStaffApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseStaffAppro
|
||||
|
||||
const PurchaseManagerApprovalObjectSchema: Yup.ObjectSchema<PurchaseRequestManagerApprovalFormSchemaType> =
|
||||
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),
|
||||
});
|
||||
|
||||
@@ -230,20 +234,6 @@ const PurchaseAcceptApprovalItemObjectSchema: Yup.ObjectSchema<PurchaseAcceptApp
|
||||
}
|
||||
)
|
||||
.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> =
|
||||
@@ -368,6 +358,7 @@ export const PurchaseRequestManagerApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
): PurchaseRequestManagerApprovalFormSchemaType => {
|
||||
return {
|
||||
action: 'APPROVED',
|
||||
notes: purchase?.notes ?? null,
|
||||
};
|
||||
};
|
||||
@@ -378,6 +369,10 @@ export type PurchaseRequestManagerApprovalFormValues = Yup.InferType<
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseRequestAcceptApprovalFormSchemaType> =
|
||||
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(PurchaseAcceptApprovalItemObjectSchema)
|
||||
@@ -388,6 +383,7 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema<PurchaseR
|
||||
|
||||
export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType =
|
||||
{
|
||||
action: 'APPROVED',
|
||||
notes: '',
|
||||
items: [
|
||||
{
|
||||
@@ -399,7 +395,6 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -408,6 +403,7 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
||||
purchase?: Purchase
|
||||
): PurchaseRequestAcceptApprovalFormSchemaType => {
|
||||
return {
|
||||
action: 'APPROVED',
|
||||
notes: purchase?.notes ?? null,
|
||||
items: purchase?.items
|
||||
? purchase.items.map((item) => ({
|
||||
@@ -419,7 +415,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
}))
|
||||
: [
|
||||
{
|
||||
@@ -431,7 +426,6 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = (
|
||||
expedition_vendor_id: 0,
|
||||
received_qty: '',
|
||||
transport_per_item: '',
|
||||
transport_total: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -61,7 +61,7 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
return 'add';
|
||||
}
|
||||
|
||||
const currentStep = initialValues?.approval?.step_number || 1;
|
||||
const currentStep = initialValues?.latest_approval?.step_number || 1;
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
@@ -77,7 +77,9 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
// Step 4+ (Penerimaan Barang dan selesai), tidak boleh edit kalau sudah disetujui
|
||||
return 'edit';
|
||||
}
|
||||
}, [rawDataApprovals, propType, initialValues?.approval?.step_number]);
|
||||
}, [rawDataApprovals, propType, initialValues?.latest_approval?.step_number]);
|
||||
|
||||
const isRejected = initialValues?.latest_approval?.action === 'REJECTED';
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -93,16 +95,16 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
|
||||
// ===== UTILITY FUNCTIONS =====
|
||||
const canUpdatePurchaseItems = useMemo(() => {
|
||||
if (!initialValues?.approval) return false;
|
||||
if (!initialValues?.latest_approval) return false;
|
||||
|
||||
const currentStep = initialValues.approval.step_number;
|
||||
const currentStep = initialValues.latest_approval.step_number;
|
||||
return currentStep >= 3;
|
||||
}, [initialValues?.approval]);
|
||||
}, [initialValues?.latest_approval]);
|
||||
|
||||
const canShowDeleteAddButtons = useMemo(() => {
|
||||
if (!initialValues?.approval) return false;
|
||||
if (!initialValues?.latest_approval) return false;
|
||||
|
||||
const currentStep = initialValues.approval.step_number;
|
||||
const currentStep = initialValues.latest_approval.step_number;
|
||||
|
||||
// Step 2 (Staff Purchase) dengan mode 'add' tidak boleh add/delete items
|
||||
// User hanya boleh input harga dan total harga untuk items yang sudah ada
|
||||
@@ -112,7 +114,7 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
|
||||
// Step 3 (Manager Purchase) boleh add/delete items
|
||||
return currentStep === 3;
|
||||
}, [initialValues?.approval, type]);
|
||||
}, [initialValues?.latest_approval, type]);
|
||||
|
||||
const isRepeaterInputError = (
|
||||
idx: number,
|
||||
@@ -719,7 +721,10 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
'min-w-52 md:min-w-72 lg:min-w-80',
|
||||
}}
|
||||
bottomLabel={
|
||||
'Previous: ' + purchaseItem.product.name
|
||||
type === 'edit'
|
||||
? 'Previous: ' +
|
||||
purchaseItem.product.name
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
@@ -819,7 +824,11 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputPrefix={'Rp'}
|
||||
bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`}
|
||||
bottomLabel={
|
||||
type === 'edit'
|
||||
? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.price || 0, 'id-ID', 2, 2)}`
|
||||
: undefined
|
||||
}
|
||||
isError={
|
||||
isRepeaterInputError(
|
||||
formItemIndex,
|
||||
@@ -857,7 +866,11 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
thousandSeparator=','
|
||||
decimalSeparator='.'
|
||||
inputPrefix={'Rp'}
|
||||
bottomLabel={`Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`}
|
||||
bottomLabel={
|
||||
type === 'edit'
|
||||
? `Previous: Rp${formatNumber(initialValues?.items?.find((item) => item.id === purchaseItem.id)?.total_price || 0, 'id-ID', 2, 2)}`
|
||||
: undefined
|
||||
}
|
||||
isError={
|
||||
isRepeaterInputError(
|
||||
formItemIndex,
|
||||
@@ -1131,7 +1144,7 @@ const PurchaseOrderStaffApprovalForm = ({
|
||||
color='primary'
|
||||
className='px-4'
|
||||
isLoading={formik.isSubmitting}
|
||||
disabled={!formik.isValid || formik.isSubmitting}
|
||||
disabled={!formik.isValid || formik.isSubmitting || isRejected}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
@@ -7,7 +7,6 @@ type PurchaseRequestFormSchemaType = {
|
||||
label: string;
|
||||
} | null;
|
||||
supplier_id: number;
|
||||
credit_term: number;
|
||||
area?: {
|
||||
value: number;
|
||||
label: string;
|
||||
@@ -78,10 +77,6 @@ export const PurchaseRequestFormSchema: Yup.ObjectSchema<PurchaseRequestFormSche
|
||||
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!')
|
||||
@@ -124,7 +119,6 @@ export const getPurchaseRequestFormInitialValues = (
|
||||
}
|
||||
: null,
|
||||
supplier_id: initialValues?.supplier?.id ?? 0,
|
||||
credit_term: initialValues?.credit_term ?? 0,
|
||||
area: initialValues?.area
|
||||
? {
|
||||
value: initialValues.area.id,
|
||||
|
||||
@@ -185,10 +185,6 @@ const PurchaseRequestForm = ({
|
||||
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,
|
||||
@@ -342,27 +338,6 @@ const PurchaseRequestForm = ({
|
||||
};
|
||||
|
||||
// ===== 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) => {
|
||||
@@ -377,16 +352,6 @@ const PurchaseRequestForm = ({
|
||||
}, []);
|
||||
|
||||
// ===== 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(
|
||||
@@ -402,23 +367,6 @@ const PurchaseRequestForm = ({
|
||||
[]
|
||||
);
|
||||
|
||||
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;
|
||||
@@ -499,7 +447,7 @@ const PurchaseRequestForm = ({
|
||||
body: 'flex flex-col gap-6',
|
||||
}}
|
||||
>
|
||||
<div className={'grid grid-cols-1 md:grid-cols-2 gap-6'}>
|
||||
<div className={'grid grid-cols-1 md:grid-cols-3 gap-6'}>
|
||||
<SelectInput
|
||||
required
|
||||
label='Vendor'
|
||||
@@ -518,29 +466,6 @@ const PurchaseRequestForm = ({
|
||||
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...'
|
||||
@@ -565,7 +490,7 @@ const PurchaseRequestForm = ({
|
||||
isClearable={type !== 'detail'}
|
||||
/>
|
||||
|
||||
<div className={'col-span-2'}>
|
||||
<div className={'md:col-span-3'}>
|
||||
<TextInput
|
||||
label='Notes'
|
||||
name='notes'
|
||||
|
||||
@@ -27,6 +27,7 @@ import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrde
|
||||
|
||||
import Card from '@/components/Card';
|
||||
import {
|
||||
CreateAcceptApprovalRequestPayload,
|
||||
CreateManagerApprovalRequestPayload,
|
||||
CreateStaffApprovalRequestPayload,
|
||||
Purchase,
|
||||
@@ -88,6 +89,8 @@ const PurchaseOrderDetail = ({
|
||||
const staffApprovalModal = useModal();
|
||||
const staffRejectionModal = useModal();
|
||||
const acceptApprovalModal = useModal();
|
||||
const acceptRejectionModal = useModal();
|
||||
const managerRejectionModal = useModal();
|
||||
const editModal = useModal();
|
||||
const penerimaanBarangModal = useModal();
|
||||
const deleteModal = useModal();
|
||||
@@ -156,9 +159,9 @@ const PurchaseOrderDetail = ({
|
||||
}, [goodsReceiptItems]);
|
||||
|
||||
const approvalStep = useMemo(() => {
|
||||
if (!initialValues?.approval) return null;
|
||||
return initialValues.approval.step_number;
|
||||
}, [initialValues?.approval]);
|
||||
if (!initialValues?.latest_approval) return null;
|
||||
return initialValues.latest_approval.step_number;
|
||||
}, [initialValues?.latest_approval]);
|
||||
|
||||
const {
|
||||
approvals,
|
||||
@@ -166,7 +169,7 @@ const PurchaseOrderDetail = ({
|
||||
rawDataApprovals,
|
||||
refresh: refreshApprovals,
|
||||
} = useApprovalSteps({
|
||||
latestApproval: initialValues?.approval,
|
||||
latestApproval: initialValues?.latest_approval,
|
||||
approvalLines: PURCHASE_ORDER_APPROVAL_LINE,
|
||||
moduleName: 'PURCHASES',
|
||||
moduleId: initialValues?.id?.toString() ?? '',
|
||||
@@ -177,19 +180,22 @@ const PurchaseOrderDetail = ({
|
||||
});
|
||||
|
||||
const showApprovalButton =
|
||||
approvalStep !== null && approvalStep >= 1 && approvalStep <= 3;
|
||||
approvalStep !== null &&
|
||||
approvalStep >= 1 &&
|
||||
approvalStep <= 3 &&
|
||||
initialValues?.latest_approval?.action !== 'REJECTED';
|
||||
|
||||
const canDeleteItems = useMemo(() => {
|
||||
if (!initialValues?.approval) return false;
|
||||
if (!initialValues?.latest_approval) return false;
|
||||
|
||||
const currentStep = initialValues.approval.step_number;
|
||||
const currentStep = initialValues.latest_approval.step_number;
|
||||
|
||||
const hasReachedStep5 = rawDataApprovals?.some(
|
||||
(approval) => approval.step_number === 5
|
||||
);
|
||||
|
||||
return currentStep === 3 && !hasReachedStep5;
|
||||
}, [initialValues?.approval, rawDataApprovals]);
|
||||
}, [initialValues?.latest_approval, rawDataApprovals]);
|
||||
|
||||
const handleApprovalClick = () => {
|
||||
if (!approvalStep) return;
|
||||
@@ -216,24 +222,30 @@ const PurchaseOrderDetail = ({
|
||||
case 1:
|
||||
staffRejectionModal.openModal();
|
||||
break;
|
||||
case 2:
|
||||
managerRejectionModal.openModal();
|
||||
break;
|
||||
case 3:
|
||||
acceptRejectionModal.openModal();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const canShowPurchaseOrderInvoice = useMemo(() => {
|
||||
if (!initialValues?.approval) return false;
|
||||
if (!initialValues?.latest_approval) return false;
|
||||
|
||||
const currentStep = initialValues.approval.step_number;
|
||||
const currentStep = initialValues.latest_approval.step_number;
|
||||
return currentStep >= 3;
|
||||
}, [initialValues?.approval]);
|
||||
}, [initialValues?.latest_approval]);
|
||||
|
||||
const canShowPenerimaanBarang = useMemo(() => {
|
||||
if (!initialValues?.approval) return false;
|
||||
if (!initialValues?.latest_approval) return false;
|
||||
|
||||
const currentStep = initialValues.approval.step_number;
|
||||
const currentStep = initialValues.latest_approval.step_number;
|
||||
return currentStep === 5;
|
||||
}, [initialValues?.approval]);
|
||||
}, [initialValues?.latest_approval]);
|
||||
|
||||
const totalBeforeTax = useMemo(() => {
|
||||
return purchaseOrderItems.reduce(
|
||||
@@ -296,6 +308,33 @@ const PurchaseOrderDetail = ({
|
||||
[initialValues?.id, searchParams, refetchData]
|
||||
);
|
||||
|
||||
const createAcceptApprovalHandler = useCallback(
|
||||
async (payload: CreateAcceptApprovalRequestPayload) => {
|
||||
const purchaseRequestId = searchParams.get('purchaseId')
|
||||
? parseInt(searchParams.get('purchaseId')!)
|
||||
: initialValues?.id || 1;
|
||||
|
||||
if (!purchaseRequestId) {
|
||||
toast.error('Purchase Request ID is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await PurchaseApi.acceptApproval.create(
|
||||
purchaseRequestId,
|
||||
payload
|
||||
);
|
||||
|
||||
if (isResponseError(res)) {
|
||||
toast.error(res.message);
|
||||
return;
|
||||
}
|
||||
toast.success(res?.message as string);
|
||||
refreshApprovals();
|
||||
refetchData?.();
|
||||
},
|
||||
[initialValues?.id, searchParams, refreshApprovals, refetchData]
|
||||
);
|
||||
|
||||
// ===== MODAL HANDLERS =====
|
||||
const handleStaffApprovalModalClose = useCallback(() => {
|
||||
refreshApprovals();
|
||||
@@ -544,11 +583,6 @@ const PurchaseOrderDetail = ({
|
||||
accessorKey: 'transport_per_item',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
},
|
||||
{
|
||||
header: 'Transport Total',
|
||||
accessorKey: 'transport_total',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
},
|
||||
];
|
||||
|
||||
const summaryData = [
|
||||
@@ -647,7 +681,7 @@ const PurchaseOrderDetail = ({
|
||||
<div className='space-y-4'>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Area
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
@@ -657,7 +691,7 @@ const PurchaseOrderDetail = ({
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Lokasi
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
@@ -671,7 +705,7 @@ const PurchaseOrderDetail = ({
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Gudang
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
@@ -685,7 +719,7 @@ const PurchaseOrderDetail = ({
|
||||
<div className='space-y-4'>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Nama Vendor
|
||||
</span>
|
||||
<span className='text-gray-900 font-medium ml-3 break-all'>
|
||||
@@ -696,7 +730,7 @@ const PurchaseOrderDetail = ({
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Kategori Vendor
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
@@ -706,18 +740,7 @@ const PurchaseOrderDetail = ({
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Tgl. Jatuh Tempo
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
: {formatDate(purchaseData.due_date, 'D MMM YYYY')} (
|
||||
{purchaseData.credit_term} hari)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Nomor
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
@@ -727,7 +750,7 @@ const PurchaseOrderDetail = ({
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
|
||||
Nomor PO
|
||||
</span>
|
||||
<div className='ml-3'>
|
||||
@@ -925,6 +948,7 @@ const PurchaseOrderDetail = ({
|
||||
color: 'success',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'APPROVED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
@@ -1041,6 +1065,61 @@ const PurchaseOrderDetail = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Accept Rejection Modal */}
|
||||
<ConfirmationModalWithNotes
|
||||
ref={acceptRejectionModal.ref}
|
||||
type='error'
|
||||
text='Apakah Anda yakin ingin menolak (reject) penerimaan barang ini?'
|
||||
placeholder='(Opsional) Masukkan alasan penolakan...'
|
||||
rows={4}
|
||||
closeOnBackdrop
|
||||
primaryButton={{
|
||||
text: 'Ya, Tolak',
|
||||
color: 'error',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateAcceptApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
items: [],
|
||||
};
|
||||
|
||||
await createAcceptApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
acceptRejectionModal.closeModal();
|
||||
},
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Manager Rejection Modal */}
|
||||
<ConfirmationModalWithNotes
|
||||
ref={managerRejectionModal.ref}
|
||||
type='error'
|
||||
text='Apakah Anda yakin ingin menolak (reject) approval manajer untuk permintaan pembelian ini?'
|
||||
placeholder='(Opsional) Masukkan alasan penolakan...'
|
||||
rows={4}
|
||||
closeOnBackdrop
|
||||
primaryButton={{
|
||||
text: 'Ya, Tolak',
|
||||
color: 'error',
|
||||
onClick: async (notes) => {
|
||||
const payload: CreateManagerApprovalRequestPayload = {
|
||||
action: 'REJECTED',
|
||||
notes: notes || null,
|
||||
};
|
||||
|
||||
await createManagerApprovalHandler(payload);
|
||||
await refetchData?.();
|
||||
managerRejectionModal.closeModal();
|
||||
},
|
||||
}}
|
||||
secondaryButton={{
|
||||
text: 'Batal',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
<ConfirmationModal
|
||||
ref={deleteModal.ref}
|
||||
|
||||
@@ -309,9 +309,6 @@ const PurchaseOrderInvoice = ({ data }: PurchaseOrderInvoiceProps) => {
|
||||
{purchaseData?.supplier?.alias || ''})
|
||||
</Text>
|
||||
<Text>{purchaseData?.supplier?.category || '-'}</Text>
|
||||
<Text>
|
||||
Credit Term: {purchaseData?.credit_term || 0} hari
|
||||
</Text>
|
||||
<Text>
|
||||
Due Date:{' '}
|
||||
{purchaseData?.due_date
|
||||
|
||||
Vendored
+4
-7
@@ -42,7 +42,6 @@ export type PurchaseItem = {
|
||||
expedition_vendor_name?: string | null;
|
||||
received_qty?: number | null;
|
||||
transport_per_item?: number | null;
|
||||
transport_total?: number | null;
|
||||
};
|
||||
|
||||
export type BasePurchase = {
|
||||
@@ -52,9 +51,7 @@ export type BasePurchase = {
|
||||
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;
|
||||
@@ -62,14 +59,13 @@ export type BasePurchase = {
|
||||
location?: Location;
|
||||
warehouse?: Warehouse;
|
||||
items?: PurchaseItem[];
|
||||
approval?: BaseApproval;
|
||||
latest_approval?: BaseApproval;
|
||||
};
|
||||
|
||||
export type Purchase = BaseMetadata & BasePurchase;
|
||||
|
||||
export type CreatePurchaseRequestPayload = {
|
||||
supplier_id: number;
|
||||
credit_term: number;
|
||||
notes?: string | null;
|
||||
items: {
|
||||
warehouse_id: number;
|
||||
@@ -103,11 +99,13 @@ export type UpdateStaffApprovalRequestPayload = {
|
||||
};
|
||||
|
||||
export type CreateManagerApprovalRequestPayload = {
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes?: string | null;
|
||||
};
|
||||
|
||||
export type CreateAcceptApprovalRequestPayload = {
|
||||
notes?: string;
|
||||
action: 'APPROVED' | 'REJECTED';
|
||||
notes?: string | null;
|
||||
items: {
|
||||
purchase_item_id: number;
|
||||
received_date: string;
|
||||
@@ -117,7 +115,6 @@ export type CreateAcceptApprovalRequestPayload = {
|
||||
expedition_vendor_id: number;
|
||||
received_qty: number;
|
||||
transport_per_item: number;
|
||||
transport_total: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user