mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-24 23:35:45 +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';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import ApprovalSteps, {
|
import ApprovalSteps, {
|
||||||
formatGroupedApprovalsToApprovalSteps,
|
formatGroupedApprovalsToApprovalSteps,
|
||||||
} from '@/components/pages/ApprovalSteps';
|
} from '@/components/pages/ApprovalSteps';
|
||||||
|
import Table from '@/components/Table';
|
||||||
|
|
||||||
import { BaseGroupedApproval } from '@/types/api/api-general';
|
import { BaseGroupedApproval } from '@/types/api/api-general';
|
||||||
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
|
||||||
|
import Card from '@/components/Card';
|
||||||
|
|
||||||
interface PurchaseOrderDetailProps {
|
interface PurchaseOrderDetailProps {
|
||||||
type?: 'detail' | 'edit';
|
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[] = [
|
const dummyGroupedApprovals: BaseGroupedApproval[] = [
|
||||||
{
|
{
|
||||||
step_number: 1,
|
step_number: 1,
|
||||||
@@ -113,6 +162,8 @@ const dummyGroupedApprovals: BaseGroupedApproval[] = [
|
|||||||
|
|
||||||
const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
|
const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
|
||||||
// ===== STATIC DATA =====
|
// ===== STATIC DATA =====
|
||||||
|
const purchaseOrderInfo = dummyPurchaseOrderInfo;
|
||||||
|
const purchaseOrderItems = dummyPurchaseOrderItems;
|
||||||
const groupedApprovals = dummyGroupedApprovals;
|
const groupedApprovals = dummyGroupedApprovals;
|
||||||
const latestApproval =
|
const latestApproval =
|
||||||
groupedApprovals[groupedApprovals.length - 1]?.approvals[0];
|
groupedApprovals[groupedApprovals.length - 1]?.approvals[0];
|
||||||
@@ -133,16 +184,270 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
|
|||||||
}
|
}
|
||||||
}, [groupedApprovals, latestApproval]);
|
}, [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 (
|
return (
|
||||||
<section className='w-full'>
|
<section className='w-full'>
|
||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
{approvalSteps.length > 0 ? (
|
{approvalSteps.length > 0 ? (
|
||||||
<ApprovalSteps approvals={approvalSteps} />
|
<div className='my-8'>
|
||||||
|
<ApprovalSteps approvals={approvalSteps} />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='text-center py-8'>
|
<div className='text-center py-8'>
|
||||||
<p className='text-gray-500'>Status approval tidak tersedia</p>
|
<p className='text-gray-500'>Status approval tidak tersedia</p>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user