Files
lti-web-client/src/components/pages/purchase/order/PurchaseOrderDetail.tsx
T

1150 lines
36 KiB
TypeScript

'use client';
import { useCallback, useMemo, useState } from 'react';
import {
ColumnDef,
SortingState,
Row,
Table as TableType,
} from '@tanstack/react-table';
import ApprovalSteps, {
useApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import Table from '@/components/Table';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
import { useModal } from '@/components/Modal';
import CheckboxInput from '@/components/input/CheckboxInput';
import Modal from '@/components/Modal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import PurchaseOrderStaffApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderStaffApprovalForm';
import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm';
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
import Card from '@/components/Card';
import {
CreateAcceptApprovalRequestPayload,
CreateManagerApprovalRequestPayload,
CreateStaffApprovalRequestPayload,
Purchase,
PurchaseItem,
} from '@/types/api/purchase/purchase';
import { PurchaseApi } from '@/services/api/purchase';
import { isResponseError } from '@/lib/api-helper';
import { toast } from 'react-hot-toast';
import { useSearchParams } from 'next/navigation';
import { formatCurrency, formatNumber, formatDate } from '@/lib/helper';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
return (
<RowOptionsMenuWrapper type='dropdown'>
<Button
onClick={onEdit}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RowOptionsMenuWrapper>
);
};
const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => {
return (
<RowOptionsMenuWrapper type='dropdown'>
<Button
onClick={onEdit}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RowOptionsMenuWrapper>
);
};
interface PurchaseOrderDetailProps {
type?: 'detail' | 'edit';
initialValues?: Purchase;
refetchData?: () => void;
}
const PurchaseOrderDetail = ({
type = 'detail',
initialValues,
refetchData,
}: PurchaseOrderDetailProps) => {
// ===== MODAL HOOKS =====
const searchParams = useSearchParams();
const confirmationModalWithNotes = useModal();
const staffApprovalModal = useModal();
const staffRejectionModal = useModal();
const acceptApprovalModal = useModal();
const acceptRejectionModal = useModal();
const managerRejectionModal = useModal();
const editModal = useModal();
const penerimaanBarangModal = useModal();
const deleteModal = useModal();
// ===== STATE MANAGEMENT =====
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [selectedItem, setSelectedItem] = useState<PurchaseItem | null>(null);
const selectedRowIds = Object.keys(rowSelection).map((item) =>
parseInt(item)
);
// ===== COMPUTED VALUES =====
const purchaseOrderItems = useMemo(
() => initialValues?.items || [],
[initialValues?.items]
);
const goodsReceiptItems = useMemo(() => {
return purchaseOrderItems.filter((item) => item.received_date);
}, [purchaseOrderItems]);
const groupedGoodsReceiptItems = useMemo(() => {
const uniqueProducts = Array.from(
new Map(
goodsReceiptItems.map((item) => [item.product?.id, item])
).values()
);
return uniqueProducts.map((item, index) => {
const productGroupItems = goodsReceiptItems.filter(
(groupItem) => groupItem.product?.id === item.product?.id
);
const totalQty = productGroupItems.reduce(
(sum, item) => sum + (item.total_qty || 0),
0
);
const receivedQty = productGroupItems.reduce(
(sum, item) => sum + (item.sub_qty || 0),
0
);
const unreceivedQty = totalQty - receivedQty;
const nominalReceived = productGroupItems.reduce(
(sum, item) => sum + (item.sub_qty || 0) * (item.price || 0),
0
);
const nominalUnreceived = productGroupItems.reduce(
(sum, item) => sum + unreceivedQty * (item.price || 0),
0
);
return {
productIndex: index + 1,
productName: item.product?.name || 'Unknown Product',
productGroupItems,
totalQty,
receivedQty,
unreceivedQty,
nominalReceived,
nominalUnreceived,
};
});
}, [goodsReceiptItems]);
const approvalStep = useMemo(() => {
if (!initialValues?.latest_approval) return null;
return initialValues.latest_approval.step_number;
}, [initialValues?.latest_approval]);
const {
approvals,
isLoading: approvalsLoading,
rawDataApprovals,
refresh: refreshApprovals,
} = useApprovalSteps({
latestApproval: initialValues?.latest_approval,
approvalLines: PURCHASE_ORDER_APPROVAL_LINE,
moduleName: 'PURCHASES',
moduleId: initialValues?.id?.toString() ?? '',
params: {
limit: 100,
group_step_number: true,
},
});
const showApprovalButton =
approvalStep !== null &&
approvalStep >= 1 &&
approvalStep <= 3 &&
initialValues?.latest_approval?.action !== 'REJECTED';
const canDeleteItems = useMemo(() => {
if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.latest_approval.step_number;
const hasReachedStep5 = rawDataApprovals?.some(
(approval) => approval.step_number === 5
);
return currentStep === 3 && !hasReachedStep5;
}, [initialValues?.latest_approval, rawDataApprovals]);
const handleApprovalClick = () => {
if (!approvalStep) return;
switch (approvalStep) {
case 1:
staffApprovalModal.openModal();
break;
case 2:
confirmationModalWithNotes.openModal();
break;
case 3:
acceptApprovalModal.openModal();
break;
default:
break;
}
};
const handleRejectionClick = () => {
if (!approvalStep) return;
switch (approvalStep) {
case 1:
staffRejectionModal.openModal();
break;
case 2:
managerRejectionModal.openModal();
break;
case 3:
acceptRejectionModal.openModal();
break;
default:
break;
}
};
const canShowPurchaseOrderInvoice = useMemo(() => {
if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.latest_approval.step_number;
return currentStep >= 3;
}, [initialValues?.latest_approval]);
const canShowPenerimaanBarang = useMemo(() => {
if (!initialValues?.latest_approval) return false;
const currentStep = initialValues.latest_approval.step_number;
return currentStep === 5;
}, [initialValues?.latest_approval]);
const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
}, [purchaseOrderItems]);
// ===== SUBMISSION HANDLER =====
const createStaffApprovalHandler = useCallback(
async (payload: CreateStaffApprovalRequestPayload) => {
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.staffApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
toast.error(res.message);
return;
}
toast.success(res?.message as string);
refreshApprovals();
refetchData?.();
},
[initialValues?.id, searchParams, refreshApprovals, refetchData]
);
const createManagerApprovalHandler = useCallback(
async (payload: CreateManagerApprovalRequestPayload) => {
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.managerApproval.create(
purchaseRequestId,
payload
);
if (isResponseError(res)) {
toast.error(res.message);
return;
}
toast.success(res?.message as string);
refetchData?.();
},
[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();
refetchData?.();
staffApprovalModal.closeModal();
}, [refreshApprovals, refetchData]);
const handleEditModalClose = useCallback(() => {
refreshApprovals();
refetchData?.();
editModal.closeModal();
}, [refreshApprovals, refetchData]);
// ===== DELETE HANDLER =====
const deleteItemsHandler = 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 itemIdsToDelete = selectedItem ? [selectedItem.id] : selectedRowIds;
if (itemIdsToDelete.length === 0) {
toast.error('Pilih minimal 1 item untuk dihapus');
return;
}
setIsDeleteLoading(true);
try {
const res = await PurchaseApi.items.delete(purchaseRequestId, {
item_ids: itemIdsToDelete,
});
if (isResponseError(res)) {
toast.error(res.message || 'Gagal menghapus item pembelian');
return;
}
const successMessage = selectedItem
? 'Berhasil menghapus item pembelian'
: `Berhasil menghapus ${itemIdsToDelete.length} item pembelian`;
toast.success(successMessage);
refreshApprovals();
refetchData?.();
deleteModal.closeModal();
setSelectedItem(null);
setRowSelection({});
} catch (error) {
toast.error('Terjadi kesalahan saat menghapus item pembelian');
} finally {
setIsDeleteLoading(false);
}
}, [
initialValues?.id,
searchParams,
selectedItem,
selectedRowIds,
refetchData,
]);
if (!initialValues) {
return null;
}
const purchaseData = initialValues;
const purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [
...(canDeleteItems
? [
{
id: 'select',
header: ({ table }: { table: TableType<PurchaseItem> }) => (
<div className='w-full flex flex-row justify-center'>
<CheckboxInput
name='allRow'
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
</div>
),
cell: ({ row }: { row: Row<PurchaseItem> }) => {
return (
<div>
<CheckboxInput
name='row'
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>
</div>
);
},
},
]
: []),
{
header: 'No',
cell: (props) => props.row.index + 1,
},
{
accessorKey: 'product.name',
header: 'Produk',
cell: (props) => props.row.original.product?.name || '-',
},
{
accessorKey: 'product.product_category',
header: 'Jenis Produk',
cell: (props) => {
const category = props.row.original.product?.product_category;
if (typeof category === 'string') {
return category;
}
return category?.name || '-';
},
},
{
accessorKey: 'sub_qty',
header: 'Jumlah',
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'product.uom.name',
header: 'Satuan',
cell: (props) => {
const uom = props.row.original.product?.uom;
if (uom && typeof uom === 'object' && uom.name) {
return uom.name;
}
return uom || '-';
},
},
{
accessorKey: 'price',
header: 'Harga Satuan',
cell: (props) => formatCurrency(props.getValue() as number),
},
{
accessorKey: 'total_price',
header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.getValue() as number),
},
...(canDeleteItems
? [
{
header: 'Aksi',
cell: (props: { row: Row<PurchaseItem> }) => {
const deleteClickHandler = () => {
setSelectedItem(props.row.original);
setRowSelection({});
deleteModal.openModal();
};
return (
<Button
type='button'
color='error'
className='text-sm'
onClick={deleteClickHandler}
>
<Icon icon='mdi:trash-can' width={16} height={16} />
</Button>
);
},
},
]
: []),
];
const goodsReceiptColumns: ColumnDef<PurchaseItem>[] = [
{
header: 'Tanggal Penerimaan',
accessorKey: 'received_date',
cell: (props) =>
props.row.original.received_date
? formatDate(props.row.original.received_date, 'DD MMM YYYY')
: '-',
},
{
header: 'Gudang Tujuan',
accessorKey: 'warehouse.name',
cell: (props) => {
const warehouse = props.row.original.warehouse;
return warehouse?.name || '-';
},
},
{
header: 'No. Surat Jalan',
accessorKey: 'travel_number',
cell: (props) => props.row.original.travel_number || '-',
},
{
header: 'Dokumen Surat Jalan',
accessorKey: 'travel_document_path',
cell: (props) => {
const documentPath = props.row.original.travel_document_path;
return documentPath ? (
<Button
color='primary'
className='w-fit min-w-32 flex items-center justify-start gap-1 px-2 py-1 text-sm'
href={documentPath}
target='_blank'
rel='noopener noreferrer'
>
<Icon
icon='material-symbols:file-open-outline'
width={16}
height={16}
/>
Lihat Dokumen
</Button>
) : (
'-'
);
},
},
{
header: 'No. Armada Pengangkut',
accessorKey: 'vehicle_number',
cell: (props) => props.row.original.vehicle_number || '-',
},
{
header: 'Jumlah Total',
accessorKey: 'sub_qty',
cell: (props) => formatNumber(props.getValue() as number),
},
{
header: 'Jumlah Diterima',
accessorKey: 'total_qty',
cell: (props) => formatNumber(props.getValue() as number),
},
{
header: 'Ekspedisi',
accessorKey: 'expedition_name',
cell: (props) => '-',
},
{
header: 'Transport /Item',
accessorKey: 'transport_per_item',
cell: (props) => formatCurrency(props.getValue() as number),
},
];
const summaryData = [
{
label: 'Total Sebelum Pajak',
value: totalBeforeTax,
},
{
label: 'Total Pembayaran',
value: totalBeforeTax,
},
];
const summaryColumns: ColumnDef<(typeof summaryData)[0]>[] = [
{
accessorKey: 'label',
header: '',
cell: (props) => (
<span className='font-semibold text-gray-700 text-sm'>
{props.getValue() as string}
</span>
),
},
{
accessorKey: 'value',
header: '',
cell: (props) => (
<span className='font-semibold text-gray-900 text-sm text-right'>
{formatCurrency(props.getValue() as number)}
</span>
),
},
];
return (
<section className='w-full'>
{/* Approval and Action Buttons */}
<div className='flex justify-between items-center w-full my-6'>
<Button
href='/purchase'
variant='link'
className='w-fit p-0 text-primary'
>
<Icon icon='uil:arrow-left' width={24} height={24} />
Kembali
</Button>
{showApprovalButton && (
<div className='flex flex-row gap-2'>
<Button
onClick={handleApprovalClick}
variant='outline'
color='success'
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
className='w-full sm:w-fit'
onClick={handleRejectionClick}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</div>
)}
</div>
{/* Steps */}
{approvals && !approvalsLoading && (
<div className='my-8'>
<ApprovalSteps approvals={approvals} />
</div>
)}
{/* Detail Purchase Order */}
<Card
collapsible
title='Detail Purchase Order'
variant='bordered'
className={{
wrapper: 'w-full',
}}
>
{/* Order Information */}
<div className='my-8'>
<h3 className='text-lg font-semibold text-gray-800 mb-6 pb-3 border-b border-gray-200'>
Informasi Pesanan
</h3>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4'>
{/* Kolom 1 */}
<div className='space-y-4'>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Area
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.items?.[0]?.warehouse?.area?.name || '-'}
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Lokasi
</span>
<span className='text-gray-900 ml-3 break-all'>
:{' '}
{purchaseData.items?.[0]?.warehouse?.type === 'LOKASI' &&
purchaseData.items?.[0]?.warehouse?.location?.name
? purchaseData.items[0].warehouse.location.name
: '-'}
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Gudang
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.items?.[0]?.warehouse?.name || '-'}
</span>
</div>
</div>
</div>
{/* Kolom 2 */}
<div className='space-y-4'>
<div className='group'>
<div className='flex items-start'>
<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'>
: {purchaseData.supplier?.name || '-'} (
{purchaseData.supplier?.alias || ''})
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Kategori Vendor
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.supplier?.category || '-'}
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Nomor
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.pr_number}
</span>
</div>
</div>
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] shrink-0'>
Nomor PO
</span>
<div className='ml-3'>
{canShowPurchaseOrderInvoice &&
purchaseData.po_number &&
purchaseData.po_number !== 'Belum dibuat' ? (
<PurchaseOrderInvoice data={purchaseData} />
) : (
<>
: <i className='text-gray-400'>Belum dibuat</i>
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
{/* Item Pembelian Section */}
<div className='mb-8'>
<div className='flex items-center justify-between mb-4 pb-2 border-b border-gray-200'>
<h3 className='text-lg font-semibold text-gray-800'>
Item Pembelian
</h3>
{canShowPurchaseOrderInvoice && (
<RowDropdownOptions isLast2Rows>
<ItemPembelianDropdown onEdit={editModal.openModal} />
</RowDropdownOptions>
)}
</div>
<div className='rounded-lg border border-gray-200 overflow-hidden'>
{/* Product Table */}
<div className='overflow-x-auto'>
<Table<PurchaseItem>
data={purchaseOrderItems}
columns={purchaseOrderColumns}
isLoading={false}
sorting={sorting}
setSorting={setSorting}
rowSelection={rowSelection}
setRowSelection={setRowSelection}
enableRowSelection={() => canDeleteItems}
className={{
containerClassName: 'm-0',
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto',
headerRowClassName: 'bg-gray-50 border-b border-gray-200',
headerColumnClassName:
'px-6 py-4 text-sm font-semibold text-gray-700 text-left',
bodyRowClassName:
'border-b border-gray-100 hover:bg-gray-50 transition-colors',
bodyColumnClassName: 'px-6 py-4 text-sm text-gray-900',
paginationClassName: 'hidden',
}}
/>
</div>
{/* Bulk Action Buttons */}
{selectedRowIds.length > 0 && canDeleteItems && (
<div className='flex justify-center items-center mt-4 gap-4 px-6 py-4 bg-gray-50 border-t border-gray-200'>
<Button
type='button'
color='error'
onClick={() => {
setSelectedItem(null);
deleteModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-fit text-sm'
>
<Icon icon='mdi:trash-can' width={16} height={16} />
Hapus Terpilih ({selectedRowIds.length})
</Button>
</div>
)}
{/* Bottom Section - Catatan dan Total */}
<div className='border-t border-gray-200 grid grid-cols-1 lg:grid-cols-5 divide-x divide-gray-200'>
{/* Catatan Section */}
<div className='lg:col-span-3 p-6 border-r border-gray-200'>
<h4 className='font-medium text-gray-700 mb-3'>Catatan</h4>
<div className='text-gray-900 min-h-[60px] italic text-sm'>
{purchaseData.notes || 'Tidak ada catatan'}
</div>
</div>
{/* Summary Section */}
<div className='lg:col-span-2 p-6'>
<Table
data={summaryData}
columns={summaryColumns}
isLoading={false}
className={{
containerClassName: 'm-0',
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto',
headerRowClassName: 'hidden',
headerColumnClassName: 'hidden',
paginationClassName: 'hidden',
bodyRowClassName:
'border-b border-gray-100 hover:bg-gray-50 transition-colors',
bodyColumnClassName: 'px-6 py-4 text-sm text-gray-900',
}}
/>
</div>
</div>
</div>
</div>
</Card>
{/* Penerimaan Barang */}
<Card
collapsible
title='Penerimaan Barang'
variant='bordered'
className={{
wrapper: 'w-full mt-8',
}}
>
{/* Detail Penerimaan Barang Section */}
<div className='my-8'>
<div className='flex items-center justify-between mb-4 pb-2 border-b border-gray-200'>
<h3 className='text-lg font-semibold text-gray-800'>
Informasi Penerimaan Barang
</h3>
{canShowPenerimaanBarang && (
<RowDropdownOptions isLast2Rows>
<PenerimaanBarangDropdown
onEdit={penerimaanBarangModal.openModal}
/>
</RowDropdownOptions>
)}
</div>
<div className='overflow-x-auto'>
{groupedGoodsReceiptItems.length > 0 ? (
<div>
{groupedGoodsReceiptItems.map((productData, index) => (
<div key={productData.productIndex}>
<div className='border border-gray-200 rounded-lg overflow-hidden mb-6'>
{/* Product Header */}
<div className='font-semibold text-gray-900 bg-gray-100 px-6 py-4 text-lg'>
{productData.productIndex}.{' '}
{productData.productName.toUpperCase()}
</div>
{/* Product Table */}
<Table<PurchaseItem>
data={productData.productGroupItems}
columns={goodsReceiptColumns}
isLoading={false}
className={{
containerClassName: 'm-0',
tableWrapperClassName: 'overflow-x-auto',
tableClassName: 'w-full table-auto',
headerRowClassName:
'bg-gray-50 border-b border-gray-200',
headerColumnClassName:
'px-4 py-3 text-sm font-semibold text-gray-700 text-left whitespace-nowrap',
bodyRowClassName:
'border-b border-gray-100 hover:bg-gray-50 transition-colors',
bodyColumnClassName:
'px-4 py-3 text-sm text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
/>
</div>
{/* Add divider after table except for last item */}
{index < groupedGoodsReceiptItems.length - 1 && (
<div className='border-t border-gray-200 my-6'></div>
)}
</div>
))}
</div>
) : (
<div className='text-center py-8 text-gray-500'>
Tidak ada data penerimaan barang
</div>
)}
</div>
</div>
</Card>
{/* Confirmation Modal with Notes */}
<ConfirmationModalWithNotes
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: async (notes) => {
const payload: CreateManagerApprovalRequestPayload = {
action: 'APPROVED',
notes: notes || null,
};
await createManagerApprovalHandler(payload);
await refreshApprovals();
await refetchData?.();
confirmationModalWithNotes.closeModal();
},
}}
secondaryButton={{
text: 'Batal',
}}
/>
{/* Staff Approval Modal */}
<Modal
ref={staffApprovalModal.ref}
closeOnBackdrop
className={{
modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto',
}}
>
<PurchaseOrderStaffApprovalForm
type='add'
initialValues={purchaseData}
onCancel={handleStaffApprovalModalClose}
refreshApprovals={refreshApprovals}
onModalClose={staffApprovalModal.closeModal}
onRefetchData={refetchData}
rawDataApprovals={rawDataApprovals}
/>
</Modal>
{/* Accept Approval Modal */}
<Modal
ref={acceptApprovalModal.ref}
closeOnBackdrop
className={{
modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto',
}}
>
<PurchaseOrderAcceptApprovalForm
type='add'
initialValues={purchaseData}
onCancel={acceptApprovalModal.closeModal}
refreshApprovals={refreshApprovals}
onModalClose={acceptApprovalModal.closeModal}
onRefetchData={refetchData}
/>
</Modal>
{/* Edit Modal */}
<Modal
ref={editModal.ref}
closeOnBackdrop
className={{
modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto',
}}
>
<PurchaseOrderStaffApprovalForm
type='edit'
initialValues={purchaseData}
onCancel={handleEditModalClose}
refreshApprovals={refreshApprovals}
onModalClose={editModal.closeModal}
onRefetchData={refetchData}
rawDataApprovals={rawDataApprovals}
/>
</Modal>
{/* Penerimaan Barang Modal */}
<Modal
ref={penerimaanBarangModal.ref}
closeOnBackdrop
className={{
modalBox: 'w-full max-w-screen-2xl max-h-[90vh] overflow-y-auto',
}}
>
<PurchaseOrderAcceptApprovalForm
type='edit'
initialValues={purchaseData}
onCancel={penerimaanBarangModal.closeModal}
refreshApprovals={refreshApprovals}
onModalClose={penerimaanBarangModal.closeModal}
onRefetchData={refetchData}
/>
</Modal>
{/* Staff Rejection Modal */}
<ConfirmationModalWithNotes
ref={staffRejectionModal.ref}
type='error'
text='Apakah Anda yakin ingin menolak (reject) permintaan pembelian ini?'
placeholder='(Opsional) Masukkan alasan penolakan...'
rows={4}
closeOnBackdrop
primaryButton={{
text: 'Ya, Tolak',
color: 'error',
onClick: async (notes) => {
const payload: CreateStaffApprovalRequestPayload = {
action: 'REJECTED',
notes: notes || null,
items: [],
};
await createStaffApprovalHandler(payload);
await refetchData?.();
staffRejectionModal.closeModal();
},
}}
secondaryButton={{
text: 'Batal',
}}
/>
{/* 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}
type='error'
text={`Apakah Anda yakin ingin menghapus ${
selectedItem
? 'item pembelian ini?'
: `${selectedRowIds.length} item pembelian yang dipilih?`
}`}
closeOnBackdrop
primaryButton={{
text: 'Ya, Hapus',
color: 'error',
isLoading: isDeleteLoading,
onClick: async () => {
await deleteItemsHandler();
},
}}
secondaryButton={{
text: 'Batal',
}}
/>
</section>
);
};
export default PurchaseOrderDetail;