refactor(FE-208): refactor PurchaseOrderDetail to integrate Purchase data and update Goods Receipt structure

This commit is contained in:
rstubryan
2025-11-14 10:11:13 +07:00
parent 1be61ae4ff
commit f98a597115
@@ -11,168 +11,173 @@ 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';
import { Purchase, PurchaseItem } from '@/types/api/purchase/purchase';
import {
createdUser,
dummyAreas,
dummyLocations,
dummyProductWarehouses,
dummyWarehouses,
} from '@/dummy/marketing.dummy';
interface PurchaseOrderDetailProps {
type?: 'detail' | 'edit';
data?: Purchase;
}
interface PurchaseOrderItem {
id: number;
product: string;
productType: string;
quantity: number;
unit: string;
unitPrice: number;
total: number;
}
interface GoodsReceiptItem {
id: number;
tanggalPenerimaan: string;
gudangTujuan: string;
noSuratJalan: string;
dokumenSuratJalan: string;
noArmada: string;
pengangkut: string;
jumlahTotal: string;
jumlahDiterima: string;
jumlahRetur: string;
ekspedisi: string;
transportPerItem: string;
transportTotal: string;
status?: 'diterima' | 'belum-diterima';
}
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 dummyPurchaseData: Purchase = {
id: 1,
pr_number: 'PR-MBU-01837',
po_number: 'Belum dibuat',
po_date: '2025-01-10T00:00:00Z',
supplier: {
id: 1,
name: 'PT. CHAROEN POKPHAND JAYA FARM',
alias: '',
pic: '',
type: '',
category: '',
hatchery: '',
phone: '',
email: '',
address: '-',
npwp: '',
account_number: '',
due_date: 0,
balance: 0,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
created_user: createdUser,
},
credit_term: 30,
due_date: '2025-11-13T00:00:00Z',
grand_total: 65000000,
notes: null,
area: dummyAreas[0],
location: dummyLocations[0],
items: [
{
id: 1,
purchase_id: 1,
product: {
id: 1,
name: 'CP Vaksin',
brand: '',
sku: '',
product_price: 0,
selling_price: 0,
tax: 0,
expiry_period: 0,
uom: {
id: 1,
name: 'Ekor',
created_user: {
id: 1,
id_user: 1,
email: 'hello@gmail.com',
name: 'Admin',
},
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
product_category: {
id: 1,
code: 'DOC',
name: 'DOC',
created_user: {
id: 1,
id_user: 1,
email: 'hello@gmail.com',
name: 'Admin',
},
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
suppliers: [],
flags: [],
created_user: {
id: 1,
id_user: 1,
email: 'hello@gmail.com',
name: 'Admin',
},
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
product_warehouse: dummyProductWarehouses[0],
quantity: 10000,
sub_qty: 10000,
total_qty: 10000,
total_used: 0,
price: 6500,
total_price: 65000000,
received_date: null,
travel_number: null,
travel_number_docs: null,
vehicle_number: null,
warehouse: dummyWarehouses[0],
},
],
created_at: '2025-01-10T00:00:00Z',
updated_at: '2025-01-10T00:00:00Z',
created_by: 1,
deleted_at: null,
created_user: createdUser,
warehouse: dummyWarehouses[0],
};
const dummyPurchaseOrderItems: PurchaseOrderItem[] = [
// Goods Receipt data - using items from PurchaseItem with received data
const dummyGoodsReceiptItems: PurchaseItem[] = [
{
id: 1,
product: 'CP Vaksin',
productType: 'DOC',
purchase_id: 1,
product: {
id: 1,
product_category: {
id: 1,
code: 'DOC',
name: 'DOC',
created_user: createdUser,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
name: 'CP Vaksin',
brand: '',
sku: '',
product_price: 0,
selling_price: 0,
tax: 0,
expiry_period: 0,
uom: {
id: 1,
name: 'Ekor',
created_user: createdUser,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
suppliers: [],
flags: [],
created_user: {
id: 1,
id_user: 1,
email: 'hello@gmail.com',
name: 'Admin',
},
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T00:00:00Z',
},
product_warehouse: dummyProductWarehouses[0],
quantity: 10000,
unit: 'Ekor',
unitPrice: 6500,
total: 65000000,
},
];
const dummyGoodsReceiptItems: GoodsReceiptItem[] = [
{
id: 1,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'details',
jumlahRetur: 'details',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp1.000,00',
},
{
id: 2,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'details',
jumlahRetur: 'Rp0',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp10.000,00',
},
{
id: 3,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'Jumlah Produk Diterima',
jumlahRetur: 'Rp0.000,00',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp10.000,00',
status: 'diterima',
},
{
id: 4,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'Jumlah Produk Belum Diterima',
jumlahRetur: 'Rp0.000,00',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp10.000,00',
status: 'belum-diterima',
},
{
id: 5,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'Nominal Produk Diterima',
jumlahRetur: 'Rp0.000,00',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp10.000,00',
status: 'diterima',
},
{
id: 6,
tanggalPenerimaan: '1 Jan 1970 00:00',
gudangTujuan: 'Gudang A',
noSuratJalan: 'NSJ-1',
dokumenSuratJalan: 'details',
noArmada: 'NAP-1',
pengangkut: 'details',
jumlahTotal: 'Rp10.000,00',
jumlahDiterima: 'Nominal Produk Belum Diterima',
jumlahRetur: 'Rp0.000,00',
ekspedisi: 'Ekspedisi 1',
transportPerItem: 'details',
transportTotal: 'Rp10.000,00',
status: 'belum-diterima',
sub_qty: 10000,
total_qty: 10000,
total_used: 0,
price: 6500,
total_price: 65000000,
received_date: '2025-01-15T00:00:00Z',
travel_number: 'NSJ-1',
travel_number_docs: '/documents/nsj-1.pdf',
vehicle_number: 'NAP-1',
warehouse: dummyWarehouses[0],
},
];
@@ -274,10 +279,13 @@ const dummyGroupedApprovals: BaseGroupedApproval[] = [
},
];
const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
const PurchaseOrderDetail = ({
type = 'detail',
data,
}: PurchaseOrderDetailProps) => {
// ===== STATIC DATA =====
const purchaseOrderInfo = dummyPurchaseOrderInfo;
const purchaseOrderItems = dummyPurchaseOrderItems;
const purchaseData = data || dummyPurchaseData;
const purchaseOrderItems = purchaseData.items || [];
const goodsReceiptItems = dummyGoodsReceiptItems;
const groupedApprovals = dummyGroupedApprovals;
const latestApproval =
@@ -300,7 +308,10 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
}, [groupedApprovals, latestApproval]);
const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce((sum, item) => sum + item.total, 0);
return purchaseOrderItems.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
}, [purchaseOrderItems]);
const formatCurrency = (value: number) => {
@@ -315,14 +326,17 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
return new Intl.NumberFormat('id-ID').format(value);
};
const purchaseOrderColumns: ColumnDef<PurchaseOrderItem>[] = [
const purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [
{
accessorKey: 'product',
accessorKey: 'product.name',
header: 'Produk',
cell: (props) => props.row.original.product?.name || '-',
},
{
accessorKey: 'productType',
accessorKey: 'product.product_category.name',
header: 'Jenis Produk',
cell: (props) =>
props.row.original.product?.product_category?.name || '-',
},
{
accessorKey: 'quantity',
@@ -330,69 +344,83 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'unit',
accessorKey: 'product.uom.name',
header: 'Satuan',
cell: (props) => props.row.original.product?.uom?.name || '-',
},
{
accessorKey: 'unitPrice',
accessorKey: 'price',
header: 'Harga Satuan',
cell: (props) => formatCurrency(props.getValue() as number),
},
{
accessorKey: 'total',
accessorKey: 'total_price',
header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.getValue() as number),
},
];
const goodsReceiptColumns: ColumnDef<GoodsReceiptItem>[] = [
const goodsReceiptColumns: ColumnDef<PurchaseItem>[] = [
{
accessorKey: 'tanggalPenerimaan',
accessorKey: 'received_date',
header: 'Tanggal Penerimaan',
cell: (props) =>
props.row.original.received_date
? new Date(props.row.original.received_date).toLocaleDateString(
'id-ID'
)
: '-',
},
{
accessorKey: 'gudangTujuan',
accessorKey: 'product_warehouse.warehouse.name',
header: 'Gudang Tujuan',
cell: (props) =>
props.row.original.product_warehouse?.warehouse?.name || '-',
},
{
accessorKey: 'noSuratJalan',
accessorKey: 'travel_number',
header: 'No. Surat Jalan',
cell: (props) => props.row.original.travel_number || '-',
},
{
accessorKey: 'dokumenSuratJalan',
accessorKey: 'travel_number_docs',
header: 'Dokumen Surat Jalan',
cell: (props) => props.row.original.travel_number_docs || '-',
},
{
accessorKey: 'noArmada',
accessorKey: 'vehicle_number',
header: 'No. Armada',
cell: (props) => props.row.original.vehicle_number || '-',
},
{
accessorKey: 'pengangkut',
header: 'Pengangkut',
cell: (props) => props.row.original.product?.name || '-',
},
{
accessorKey: 'jumlahTotal',
accessorKey: 'quantity',
header: 'Jumlah Total',
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'jumlahDiterima',
accessorKey: 'sub_qty',
header: 'Jumlah Diterima',
},
{
accessorKey: 'jumlahRetur',
header: 'Jumlah Retur',
cell: (props) => formatNumber(props.getValue() as number),
},
{
accessorKey: 'ekspedisi',
header: 'Ekspedisi',
cell: (props) => 'Ekspedisi 1',
},
{
accessorKey: 'transportPerItem',
accessorKey: 'price',
header: 'Transport /Item',
cell: (props) => formatCurrency(props.getValue() as number),
},
{
accessorKey: 'transportTotal',
accessorKey: 'total_price',
header: 'Transport Total',
cell: (props) => formatCurrency(props.getValue() as number),
},
];
@@ -458,23 +486,13 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
<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}
{purchaseData.area?.name || '-'}
</span>
</div>
</div>
@@ -484,7 +502,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Lokasi
</span>
<span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.location}
{purchaseData.location?.name || '-'}
</span>
</div>
</div>
@@ -494,7 +512,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Gudang Penyimpanan
</span>
<span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.warehouse}
{purchaseData.warehouse?.name || '-'}
</span>
</div>
</div>
@@ -505,20 +523,20 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Nama Vendor
Nama Supplier
</span>
<span className='text-gray-900 font-medium ml-3 break-all'>
{purchaseOrderInfo.vendorName}
{purchaseData.supplier?.name || '-'}
</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
Alamat Supplier
</span>
<span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.vendorAddress}
{purchaseData.supplier?.address || '-'}
</span>
</div>
</div>
@@ -528,7 +546,15 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Tgl. Jatuh Tempo
</span>
<span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.dueDate}
{new Date(purchaseData.due_date).toLocaleDateString(
'id-ID',
{
day: 'numeric',
month: 'short',
year: 'numeric',
}
)}{' '}
({purchaseData.credit_term} hari)
</span>
</div>
</div>
@@ -538,7 +564,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Nomor
</span>
<span className='text-gray-900 ml-3 break-all font-mono text-sm'>
{purchaseOrderInfo.number}
{purchaseData.pr_number}
</span>
</div>
</div>
@@ -548,7 +574,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Nomor PO
</span>
<span className='text-gray-900 ml-3 break-all font-mono text-sm'>
{purchaseOrderInfo.poNumber}
{purchaseData.po_number || 'Belum dibuat'}
</span>
</div>
</div>
@@ -564,7 +590,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
<div className='rounded-lg border border-gray-200 overflow-hidden'>
{/* Product Table */}
<div className='overflow-x-auto'>
<Table<PurchaseOrderItem>
<Table<PurchaseItem>
data={purchaseOrderItems}
columns={purchaseOrderColumns}
isLoading={false}
@@ -588,7 +614,9 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
{/* 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 className='text-gray-900 min-h-[60px] italic text-sm'>
{purchaseData.notes || 'Tidak ada catatan'}
</div>
</div>
{/* Summary Section */}
@@ -626,7 +654,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
>
<div className='rounded-lg border border-gray-200 overflow-hidden'>
<div className='overflow-x-auto'>
<Table<GoodsReceiptItem>
<Table<PurchaseItem>
data={goodsReceiptItems}
columns={goodsReceiptColumns}
isLoading={false}
@@ -639,7 +667,8 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
'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',
bodyColumnClassName:
'px-4 py-3 text-sm text-gray-900 whitespace-nowrap',
paginationClassName: 'hidden',
}}
/>