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:
Adnan Zahir
2025-12-10 23:18:50 +07:00
9 changed files with 168 additions and 239 deletions
@@ -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
+4 -7
View File
@@ -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;
}[];
};