Merge branch 'development' into 'production'

Development

See merge request mbugroup/lti-web-client!435
This commit is contained in:
Adnan Zahir
2026-04-26 00:13:15 +07:00
4 changed files with 230 additions and 21 deletions
@@ -305,6 +305,14 @@ const PurchaseTable = () => {
? formatDate(props.row.original.po_date, 'DD MMM YYYY')
: '-',
},
{
accessorKey: 'received_date',
header: 'Tgl. Terima',
cell: (props) =>
props.row.original.received_date
? formatDate(props.row.original.received_date, 'DD MMM YYYY')
: '-',
},
{
accessorKey: 'due_date',
header: 'Jatuh Tempo',
@@ -26,6 +26,8 @@ import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/or
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
import Card from '@/components/Card';
import DateInput from '@/components/input/DateInput';
import TextArea from '@/components/input/TextArea';
import {
CreateAcceptApprovalRequestPayload,
CreateManagerApprovalRequestPayload,
@@ -96,6 +98,7 @@ const PurchaseOrderDetail = ({
const acceptRejectionModal = useModal();
const managerRejectionModal = useModal();
const editModal = useModal();
const editPoDateModal = useModal();
const penerimaanBarangModal = useModal();
const deleteModal = useModal();
@@ -105,6 +108,9 @@ const PurchaseOrderDetail = ({
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
const [, setApprovalNotes] = useState('');
const [managerApprovalNotes, setManagerApprovalNotes] = useState('');
const [managerApprovalPoDate, setManagerApprovalPoDate] = useState('');
const [editPoDate, setEditPoDate] = useState('');
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
@@ -212,6 +218,8 @@ const PurchaseOrderDetail = ({
break;
case 2:
setApprovalNotes('');
setManagerApprovalNotes('');
setManagerApprovalPoDate('');
confirmationModalWithNotes.openModal();
break;
case 3:
@@ -414,17 +422,44 @@ const PurchaseOrderDetail = ({
deleteModal,
]);
const updatePoDateHandler = useCallback(async () => {
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.updatePoDate(purchaseRequestId, {
po_date: editPoDate,
});
if (isResponseError(res)) {
toast.error(res.message || 'Gagal mengubah tanggal PO');
return;
}
toast.success('Tanggal PO berhasil diubah');
setEditPoDate('');
editPoDateModal.closeModal();
refetchData?.();
}, [initialValues?.id, searchParams, editPoDate, editPoDateModal, refetchData]);
// ===== APPROVAL/REJECTION HANDLERS =====
const managerApprovalHandler = async (notes: string) => {
const managerApprovalHandler = async () => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'APPROVED',
notes: notes || null,
notes: managerApprovalNotes || null,
po_date: managerApprovalPoDate || null,
};
await createManagerApprovalHandler(payload);
await refreshApprovals();
await refetchData?.();
setApprovalNotes('');
setManagerApprovalNotes('');
setManagerApprovalPoDate('');
confirmationModalWithNotes.closeModal();
};
@@ -829,6 +864,45 @@ const PurchaseOrderDetail = ({
</div>
</div>
</div>
{purchaseData.po_date &&
!purchaseData.po_date.startsWith('0001') && (
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Tanggal PO
</span>
<div className='ml-3 flex items-center gap-1'>
<span className='text-gray-900'>
:{' '}
{formatDate(purchaseData.po_date, 'DD MMM YYYY')}
</span>
<RequirePermission permissions='lti.purchase.update'>
<Button
type='button'
variant='ghost'
color='warning'
className='p-1 min-h-0 h-auto'
onClick={() => {
setEditPoDate(
formatDate(
purchaseData.po_date,
'YYYY-MM-DD'
)
);
editPoDateModal.openModal();
}}
>
<Icon
icon='material-symbols:edit-outline'
width={14}
height={14}
/>
</Button>
</RequirePermission>
</div>
</div>
</div>
)}
</div>
</div>
</div>
@@ -1016,28 +1090,80 @@ const PurchaseOrderDetail = ({
</div>
</Card>
{/* Confirmation Modal with Notes */}
<ConfirmationModalWithNotes
{/* Manager Approval Modal */}
<Modal
ref={confirmationModalWithNotes.ref}
type='success'
text='Apakah Anda yakin ingin melanjutkan approval ini?'
placeholder='(Opsional) Tambahkan catatan untuk approval ini...'
rows={4}
closeOnBackdrop
primaryButton={{
text: 'Ya, Lanjutkan',
color: 'success',
onClick: managerApprovalHandler,
className={{
modalBox: 'max-w-lg rounded-lg p-0',
}}
secondaryButton={{
text: 'Batal',
onClick: () => {
setApprovalNotes('');
>
<div className='flex flex-col'>
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
<h4 className='text-sm font-semibold text-base-content'>
Konfirmasi Approval Manager
</h4>
<Button
variant='ghost'
color='none'
onClick={() => {
setManagerApprovalNotes('');
setManagerApprovalPoDate('');
confirmationModalWithNotes.closeModal();
},
}}
className='p-1'
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div className='flex flex-col gap-4 p-4'>
<p className='text-sm text-base-content/70'>
Apakah Anda yakin ingin melanjutkan approval ini?
</p>
<DateInput
name='manager_approval_po_date'
label='Tanggal PO'
value={managerApprovalPoDate}
onChange={(e) => setManagerApprovalPoDate(e.target.value)}
isNestedModal
/>
<TextArea
name='manager_approval_notes'
label='Catatan (Opsional)'
placeholder='Tambahkan catatan untuk approval ini...'
value={managerApprovalNotes}
onChange={(e) => setManagerApprovalNotes(e.target.value)}
rows={4}
/>
</div>
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
<Button
variant='outline'
color='none'
onClick={() => {
setManagerApprovalNotes('');
setManagerApprovalPoDate('');
confirmationModalWithNotes.closeModal();
}}
className='px-3 py-2.5'
>
Batal
</Button>
<Button
color='success'
onClick={managerApprovalHandler}
className='px-3 py-2.5'
>
Ya, Lanjutkan
</Button>
</div>
</div>
</Modal>
{/* Staff Approval Modal */}
<Modal
ref={staffApprovalModal.ref}
@@ -1112,6 +1238,66 @@ const PurchaseOrderDetail = ({
/>
</Modal>
{/* Edit PO Date Modal */}
<Modal
ref={editPoDateModal.ref}
closeOnBackdrop
className={{
modalBox: 'max-w-sm rounded-lg p-0',
}}
>
<div className='flex flex-col'>
<div className='flex items-center justify-between border-b border-base-content/10 p-4'>
<h4 className='text-sm font-semibold text-base-content'>
Edit Tanggal PO
</h4>
<Button
variant='ghost'
color='none'
onClick={() => {
setEditPoDate('');
editPoDateModal.closeModal();
}}
className='p-1'
>
<Icon icon='mdi:close' width={20} height={20} />
</Button>
</div>
<div className='flex flex-col gap-4 p-4'>
<DateInput
name='edit_po_date'
label='Tanggal PO'
value={editPoDate}
onChange={(e) => setEditPoDate(e.target.value)}
isNestedModal
/>
</div>
<div className='flex justify-end gap-3 border-t border-base-content/10 p-4'>
<Button
variant='outline'
color='none'
onClick={() => {
setEditPoDate('');
editPoDateModal.closeModal();
}}
className='px-3 py-2.5'
>
Batal
</Button>
<Button
color='primary'
onClick={updatePoDateHandler}
className='px-3 py-2.5'
disabled={!editPoDate}
>
Simpan
</Button>
</div>
</div>
</Modal>
{/* Staff Rejection Modal */}
<ConfirmationModalWithNotes
ref={staffRejectionModal.ref}
+13
View File
@@ -115,6 +115,19 @@ export const PurchaseApi = {
},
},
updatePoDate: async (
purchaseRequestId: number,
payload: { po_date: string }
): Promise<BaseApiResponse<Purchase> | undefined> => {
return await basePurchaseApi.customRequest<BaseApiResponse<Purchase>>(
`${purchaseRequestId}/po-date`,
{
method: 'PATCH',
payload,
}
);
},
async exportToExcel(initialQueryString: string) {
const params = new URLSearchParams(initialQueryString);
+2
View File
@@ -68,6 +68,7 @@ export type BasePurchase = {
supplier: Supplier;
credit_term?: number;
due_date: string;
received_date?: string | null;
notes?: string | null;
deleted_at?: string | null;
created_by: number;
@@ -122,6 +123,7 @@ export type UpdateStaffApprovalRequestPayload = {
export type CreateManagerApprovalRequestPayload = {
action: 'APPROVED' | 'REJECTED';
notes?: string | null;
po_date?: string | null;
};
export type CreateAcceptApprovalRequestPayload = {