mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-208): implement purchase order detail view with collapsible sections and summary table
This commit is contained in:
@@ -1,18 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import ApprovalSteps, {
|
||||
formatGroupedApprovalsToApprovalSteps,
|
||||
} from '@/components/pages/ApprovalSteps';
|
||||
import Table from '@/components/Table';
|
||||
|
||||
import { BaseGroupedApproval } from '@/types/api/api-general';
|
||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||
import Card from '@/components/Card';
|
||||
|
||||
interface PurchaseOrderDetailProps {
|
||||
type?: 'detail' | 'edit';
|
||||
}
|
||||
|
||||
interface PurchaseOrderItem {
|
||||
id: number;
|
||||
product: string;
|
||||
productType: string;
|
||||
quantity: number;
|
||||
unit: string;
|
||||
unitPrice: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface PurchaseOrderInfo {
|
||||
businessUnit: string;
|
||||
area: string;
|
||||
location: string;
|
||||
warehouse: string;
|
||||
vendorName: string;
|
||||
vendorAddress: string;
|
||||
dueDate: string;
|
||||
number: string;
|
||||
poNumber: string;
|
||||
}
|
||||
|
||||
const dummyPurchaseOrderInfo: PurchaseOrderInfo = {
|
||||
businessUnit: 'PT MITRA BERLIAN UNGGAS',
|
||||
area: 'Banten 2',
|
||||
location: 'FARM PASIR TAPLOK',
|
||||
warehouse: 'GUDANG PASIR TAPLOK 1',
|
||||
vendorName: 'PT. CHAROEN POKPHAND JAYA FARM',
|
||||
vendorAddress: '-',
|
||||
dueDate: '13-Nov-2025 (1 hari)',
|
||||
number: 'PR-MBU-01837',
|
||||
poNumber: 'Belum dibuat',
|
||||
};
|
||||
|
||||
const dummyPurchaseOrderItems: PurchaseOrderItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
product: 'CP Vaksin',
|
||||
productType: 'DOC',
|
||||
quantity: 10000,
|
||||
unit: 'Ekor',
|
||||
unitPrice: 6500,
|
||||
total: 65000000,
|
||||
},
|
||||
];
|
||||
|
||||
const dummyGroupedApprovals: BaseGroupedApproval[] = [
|
||||
{
|
||||
step_number: 1,
|
||||
@@ -113,6 +162,8 @@ const dummyGroupedApprovals: BaseGroupedApproval[] = [
|
||||
|
||||
const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
|
||||
// ===== STATIC DATA =====
|
||||
const purchaseOrderInfo = dummyPurchaseOrderInfo;
|
||||
const purchaseOrderItems = dummyPurchaseOrderItems;
|
||||
const groupedApprovals = dummyGroupedApprovals;
|
||||
const latestApproval =
|
||||
groupedApprovals[groupedApprovals.length - 1]?.approvals[0];
|
||||
@@ -133,16 +184,270 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
|
||||
}
|
||||
}, [groupedApprovals, latestApproval]);
|
||||
|
||||
const totalBeforeTax = useMemo(() => {
|
||||
return purchaseOrderItems.reduce((sum, item) => sum + item.total, 0);
|
||||
}, [purchaseOrderItems]);
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatNumber = (value: number) => {
|
||||
return new Intl.NumberFormat('id-ID').format(value);
|
||||
};
|
||||
|
||||
const purchaseOrderColumns: ColumnDef<PurchaseOrderItem>[] = [
|
||||
{
|
||||
accessorKey: 'product',
|
||||
header: 'Produk',
|
||||
},
|
||||
{
|
||||
accessorKey: 'productType',
|
||||
header: 'Jenis Produk',
|
||||
},
|
||||
{
|
||||
accessorKey: 'quantity',
|
||||
header: 'Jumlah',
|
||||
cell: (props) => formatNumber(props.getValue() as number),
|
||||
},
|
||||
{
|
||||
accessorKey: 'unit',
|
||||
header: 'Satuan',
|
||||
},
|
||||
{
|
||||
accessorKey: 'unitPrice',
|
||||
header: 'Harga Satuan',
|
||||
cell: (props) => formatCurrency(props.getValue() as number),
|
||||
},
|
||||
{
|
||||
accessorKey: 'total',
|
||||
header: 'Total (Rp.)',
|
||||
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'>
|
||||
{/* Steps */}
|
||||
{approvalSteps.length > 0 ? (
|
||||
<div className='my-8'>
|
||||
<ApprovalSteps approvals={approvalSteps} />
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-center py-8'>
|
||||
<p className='text-gray-500'>Status approval tidak tersedia</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Detail Purchase Order */}
|
||||
<Card
|
||||
collapsible
|
||||
title='Detail Purchase Order'
|
||||
variant='bordered'
|
||||
className={{
|
||||
wrapper: 'w-full',
|
||||
}}
|
||||
>
|
||||
{/* Order Information */}
|
||||
<div className='bg-gray-50 rounded-lg p-6 mb-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] flex-shrink-0'>
|
||||
Unit Bisnis
|
||||
</span>
|
||||
<span className='text-gray-900 font-medium ml-3 break-all'>
|
||||
{purchaseOrderInfo.businessUnit}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Area
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
{purchaseOrderInfo.area}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Lokasi
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
{purchaseOrderInfo.location}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Gudang Penyimpanan
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
{purchaseOrderInfo.warehouse}
|
||||
</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] flex-shrink-0'>
|
||||
Nama Vendor
|
||||
</span>
|
||||
<span className='text-gray-900 font-medium ml-3 break-all'>
|
||||
{purchaseOrderInfo.vendorName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Alamat Vendor
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all'>
|
||||
{purchaseOrderInfo.vendorAddress}
|
||||
</span>
|
||||
</div>
|
||||
</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'>
|
||||
{purchaseOrderInfo.dueDate}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Nomor
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all font-mono text-sm'>
|
||||
{purchaseOrderInfo.number}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='group'>
|
||||
<div className='flex items-start'>
|
||||
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
|
||||
Nomor PO
|
||||
</span>
|
||||
<span className='text-gray-900 ml-3 break-all font-mono text-sm'>
|
||||
{purchaseOrderInfo.poNumber}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Item Pembelian Section */}
|
||||
<div className='mb-8'>
|
||||
<h3 className='text-lg font-semibold text-gray-800 mb-4 pb-2 border-b border-gray-200'>
|
||||
Item Pembelian
|
||||
</h3>
|
||||
<div className='rounded-lg border border-gray-200 overflow-hidden'>
|
||||
{/* Product Table */}
|
||||
<div className='overflow-x-auto'>
|
||||
<Table<PurchaseOrderItem>
|
||||
data={purchaseOrderItems}
|
||||
columns={purchaseOrderColumns}
|
||||
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-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>
|
||||
|
||||
{/* 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'></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>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user