mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
1175 lines
37 KiB
TypeScript
1175 lines
37 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';
|
|
import RequirePermission from '@/components/helper/RequirePermission';
|
|
|
|
const ItemPembelianDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
|
return (
|
|
<RowOptionsMenuWrapper type='dropdown'>
|
|
<RequirePermission permissions='lti.purchase.update'>
|
|
<Button
|
|
onClick={onEdit}
|
|
variant='ghost'
|
|
color='warning'
|
|
className='justify-start text-sm'
|
|
>
|
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
</RowOptionsMenuWrapper>
|
|
);
|
|
};
|
|
|
|
const PenerimaanBarangDropdown = ({ onEdit }: { onEdit: () => void }) => {
|
|
return (
|
|
<RowOptionsMenuWrapper type='dropdown'>
|
|
<RequirePermission permissions='lti.purchase.receive'>
|
|
<Button
|
|
onClick={onEdit}
|
|
variant='ghost'
|
|
color='warning'
|
|
className='justify-start text-sm'
|
|
>
|
|
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
|
|
Edit
|
|
</Button>
|
|
</RequirePermission>
|
|
</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 (
|
|
<RequirePermission permissions='lti.purchase.delete.item'>
|
|
<Button
|
|
type='button'
|
|
color='error'
|
|
className='text-sm'
|
|
onClick={deleteClickHandler}
|
|
>
|
|
<Icon icon='mdi:trash-can' width={16} height={16} />
|
|
</Button>
|
|
</RequirePermission>
|
|
);
|
|
},
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
|
|
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'>
|
|
<RequirePermission
|
|
permissions={
|
|
approvalStep === 1
|
|
? 'lti.purchase.approve.staff'
|
|
: approvalStep === 2
|
|
? 'lti.purchase.approve.manager'
|
|
: 'lti.purchase.receive'
|
|
}
|
|
>
|
|
<Button
|
|
onClick={handleApprovalClick}
|
|
variant='outline'
|
|
color='success'
|
|
className='w-full sm:w-fit'
|
|
>
|
|
<Icon icon='material-symbols:check' width={24} height={24} />
|
|
Approve
|
|
</Button>
|
|
</RequirePermission>
|
|
|
|
<RequirePermission
|
|
permissions={
|
|
approvalStep === 1
|
|
? 'lti.purchase.approve.staff'
|
|
: approvalStep === 2
|
|
? 'lti.purchase.approve.manager'
|
|
: 'lti.purchase.receive'
|
|
}
|
|
>
|
|
<Button
|
|
variant='outline'
|
|
color='error'
|
|
className='w-full sm:w-fit'
|
|
onClick={handleRejectionClick}
|
|
>
|
|
<Icon icon='material-symbols:close' width={24} height={24} />
|
|
Reject
|
|
</Button>
|
|
</RequirePermission>
|
|
</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,
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
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;
|