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 { 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'; 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 { interface PurchaseOrderDetailProps {
type?: 'detail' | 'edit'; type?: 'detail' | 'edit';
data?: Purchase;
} }
interface PurchaseOrderItem { const dummyPurchaseData: Purchase = {
id: number; id: 1,
product: string; pr_number: 'PR-MBU-01837',
productType: string; po_number: 'Belum dibuat',
quantity: number; po_date: '2025-01-10T00:00:00Z',
unit: string; supplier: {
unitPrice: number; id: 1,
total: number; name: 'PT. CHAROEN POKPHAND JAYA FARM',
} alias: '',
pic: '',
interface GoodsReceiptItem { type: '',
id: number; category: '',
tanggalPenerimaan: string; hatchery: '',
gudangTujuan: string; phone: '',
noSuratJalan: string; email: '',
dokumenSuratJalan: string; address: '-',
noArmada: string; npwp: '',
pengangkut: string; account_number: '',
jumlahTotal: string; due_date: 0,
jumlahDiterima: string; balance: 0,
jumlahRetur: string; created_at: '2025-01-01T00:00:00Z',
ekspedisi: string; updated_at: '2025-01-01T00:00:00Z',
transportPerItem: string; created_user: createdUser,
transportTotal: string; },
status?: 'diterima' | 'belum-diterima'; credit_term: 30,
} due_date: '2025-11-13T00:00:00Z',
grand_total: 65000000,
interface PurchaseOrderInfo { notes: null,
businessUnit: string; area: dummyAreas[0],
area: string; location: dummyLocations[0],
location: string; items: [
warehouse: string; {
vendorName: string; id: 1,
vendorAddress: string; purchase_id: 1,
dueDate: string; product: {
number: string; id: 1,
poNumber: string; name: 'CP Vaksin',
} brand: '',
sku: '',
const dummyPurchaseOrderInfo: PurchaseOrderInfo = { product_price: 0,
businessUnit: 'PT MITRA BERLIAN UNGGAS', selling_price: 0,
area: 'Banten 2', tax: 0,
location: 'FARM PASIR TAPLOK', expiry_period: 0,
warehouse: 'GUDANG PASIR TAPLOK 1', uom: {
vendorName: 'PT. CHAROEN POKPHAND JAYA FARM', id: 1,
vendorAddress: '-', name: 'Ekor',
dueDate: '13-Nov-2025 (1 hari)', created_user: {
number: 'PR-MBU-01837', id: 1,
poNumber: 'Belum dibuat', 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, id: 1,
product: 'CP Vaksin', purchase_id: 1,
productType: 'DOC', 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, quantity: 10000,
unit: 'Ekor', sub_qty: 10000,
unitPrice: 6500, total_qty: 10000,
total: 65000000, total_used: 0,
}, price: 6500,
]; total_price: 65000000,
received_date: '2025-01-15T00:00:00Z',
const dummyGoodsReceiptItems: GoodsReceiptItem[] = [ travel_number: 'NSJ-1',
{ travel_number_docs: '/documents/nsj-1.pdf',
id: 1, vehicle_number: 'NAP-1',
tanggalPenerimaan: '1 Jan 1970 00:00', warehouse: dummyWarehouses[0],
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',
}, },
]; ];
@@ -274,10 +279,13 @@ const dummyGroupedApprovals: BaseGroupedApproval[] = [
}, },
]; ];
const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => { const PurchaseOrderDetail = ({
type = 'detail',
data,
}: PurchaseOrderDetailProps) => {
// ===== STATIC DATA ===== // ===== STATIC DATA =====
const purchaseOrderInfo = dummyPurchaseOrderInfo; const purchaseData = data || dummyPurchaseData;
const purchaseOrderItems = dummyPurchaseOrderItems; const purchaseOrderItems = purchaseData.items || [];
const goodsReceiptItems = dummyGoodsReceiptItems; const goodsReceiptItems = dummyGoodsReceiptItems;
const groupedApprovals = dummyGroupedApprovals; const groupedApprovals = dummyGroupedApprovals;
const latestApproval = const latestApproval =
@@ -300,7 +308,10 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
}, [groupedApprovals, latestApproval]); }, [groupedApprovals, latestApproval]);
const totalBeforeTax = useMemo(() => { const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce((sum, item) => sum + item.total, 0); return purchaseOrderItems.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
}, [purchaseOrderItems]); }, [purchaseOrderItems]);
const formatCurrency = (value: number) => { const formatCurrency = (value: number) => {
@@ -315,14 +326,17 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
return new Intl.NumberFormat('id-ID').format(value); return new Intl.NumberFormat('id-ID').format(value);
}; };
const purchaseOrderColumns: ColumnDef<PurchaseOrderItem>[] = [ const purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [
{ {
accessorKey: 'product', accessorKey: 'product.name',
header: 'Produk', header: 'Produk',
cell: (props) => props.row.original.product?.name || '-',
}, },
{ {
accessorKey: 'productType', accessorKey: 'product.product_category.name',
header: 'Jenis Produk', header: 'Jenis Produk',
cell: (props) =>
props.row.original.product?.product_category?.name || '-',
}, },
{ {
accessorKey: 'quantity', accessorKey: 'quantity',
@@ -330,69 +344,83 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
cell: (props) => formatNumber(props.getValue() as number), cell: (props) => formatNumber(props.getValue() as number),
}, },
{ {
accessorKey: 'unit', accessorKey: 'product.uom.name',
header: 'Satuan', header: 'Satuan',
cell: (props) => props.row.original.product?.uom?.name || '-',
}, },
{ {
accessorKey: 'unitPrice', accessorKey: 'price',
header: 'Harga Satuan', header: 'Harga Satuan',
cell: (props) => formatCurrency(props.getValue() as number), cell: (props) => formatCurrency(props.getValue() as number),
}, },
{ {
accessorKey: 'total', accessorKey: 'total_price',
header: 'Total (Rp.)', header: 'Total (Rp.)',
cell: (props) => formatCurrency(props.getValue() as number), cell: (props) => formatCurrency(props.getValue() as number),
}, },
]; ];
const goodsReceiptColumns: ColumnDef<GoodsReceiptItem>[] = [ const goodsReceiptColumns: ColumnDef<PurchaseItem>[] = [
{ {
accessorKey: 'tanggalPenerimaan', accessorKey: 'received_date',
header: 'Tanggal Penerimaan', 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', header: 'Gudang Tujuan',
cell: (props) =>
props.row.original.product_warehouse?.warehouse?.name || '-',
}, },
{ {
accessorKey: 'noSuratJalan', accessorKey: 'travel_number',
header: 'No. Surat Jalan', header: 'No. Surat Jalan',
cell: (props) => props.row.original.travel_number || '-',
}, },
{ {
accessorKey: 'dokumenSuratJalan', accessorKey: 'travel_number_docs',
header: 'Dokumen Surat Jalan', header: 'Dokumen Surat Jalan',
cell: (props) => props.row.original.travel_number_docs || '-',
}, },
{ {
accessorKey: 'noArmada', accessorKey: 'vehicle_number',
header: 'No. Armada', header: 'No. Armada',
cell: (props) => props.row.original.vehicle_number || '-',
}, },
{ {
accessorKey: 'pengangkut', accessorKey: 'pengangkut',
header: 'Pengangkut', header: 'Pengangkut',
cell: (props) => props.row.original.product?.name || '-',
}, },
{ {
accessorKey: 'jumlahTotal', accessorKey: 'quantity',
header: 'Jumlah Total', header: 'Jumlah Total',
cell: (props) => formatNumber(props.getValue() as number),
}, },
{ {
accessorKey: 'jumlahDiterima', accessorKey: 'sub_qty',
header: 'Jumlah Diterima', header: 'Jumlah Diterima',
}, cell: (props) => formatNumber(props.getValue() as number),
{
accessorKey: 'jumlahRetur',
header: 'Jumlah Retur',
}, },
{ {
accessorKey: 'ekspedisi', accessorKey: 'ekspedisi',
header: 'Ekspedisi', header: 'Ekspedisi',
cell: (props) => 'Ekspedisi 1',
}, },
{ {
accessorKey: 'transportPerItem', accessorKey: 'price',
header: 'Transport /Item', header: 'Transport /Item',
cell: (props) => formatCurrency(props.getValue() as number),
}, },
{ {
accessorKey: 'transportTotal', accessorKey: 'total_price',
header: 'Transport Total', 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'> <div className='grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4'>
{/* Kolom 1 */} {/* Kolom 1 */}
<div className='space-y-4'> <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='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Area Area
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.area} {purchaseData.area?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -484,7 +502,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Lokasi Lokasi
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.location} {purchaseData.location?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -494,7 +512,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Gudang Penyimpanan Gudang Penyimpanan
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.warehouse} {purchaseData.warehouse?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -505,20 +523,20 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Nama Vendor Nama Supplier
</span> </span>
<span className='text-gray-900 font-medium ml-3 break-all'> <span className='text-gray-900 font-medium ml-3 break-all'>
{purchaseOrderInfo.vendorName} {purchaseData.supplier?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
<div className='group'> <div className='group'>
<div className='flex items-start'> <div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'> <span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Alamat Vendor Alamat Supplier
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
{purchaseOrderInfo.vendorAddress} {purchaseData.supplier?.address || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -528,7 +546,15 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Tgl. Jatuh Tempo Tgl. Jatuh Tempo
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <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> </span>
</div> </div>
</div> </div>
@@ -538,7 +564,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Nomor Nomor
</span> </span>
<span className='text-gray-900 ml-3 break-all font-mono text-sm'> <span className='text-gray-900 ml-3 break-all font-mono text-sm'>
{purchaseOrderInfo.number} {purchaseData.pr_number}
</span> </span>
</div> </div>
</div> </div>
@@ -548,7 +574,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
Nomor PO Nomor PO
</span> </span>
<span className='text-gray-900 ml-3 break-all font-mono text-sm'> <span className='text-gray-900 ml-3 break-all font-mono text-sm'>
{purchaseOrderInfo.poNumber} {purchaseData.po_number || 'Belum dibuat'}
</span> </span>
</div> </div>
</div> </div>
@@ -564,7 +590,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
<div className='rounded-lg border border-gray-200 overflow-hidden'> <div className='rounded-lg border border-gray-200 overflow-hidden'>
{/* Product Table */} {/* Product Table */}
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<Table<PurchaseOrderItem> <Table<PurchaseItem>
data={purchaseOrderItems} data={purchaseOrderItems}
columns={purchaseOrderColumns} columns={purchaseOrderColumns}
isLoading={false} isLoading={false}
@@ -588,7 +614,9 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
{/* Catatan Section */} {/* Catatan Section */}
<div className='lg:col-span-3 p-6 border-r border-gray-200'> <div className='lg:col-span-3 p-6 border-r border-gray-200'>
<h4 className='font-medium text-gray-700 mb-3'>Catatan</h4> <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> </div>
{/* Summary Section */} {/* Summary Section */}
@@ -626,7 +654,7 @@ const PurchaseOrderDetail = ({ type = 'detail' }: PurchaseOrderDetailProps) => {
> >
<div className='rounded-lg border border-gray-200 overflow-hidden'> <div className='rounded-lg border border-gray-200 overflow-hidden'>
<div className='overflow-x-auto'> <div className='overflow-x-auto'>
<Table<GoodsReceiptItem> <Table<PurchaseItem>
data={goodsReceiptItems} data={goodsReceiptItems}
columns={goodsReceiptColumns} columns={goodsReceiptColumns}
isLoading={false} 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', 'px-4 py-3 text-sm font-semibold text-gray-700 text-left whitespace-nowrap',
bodyRowClassName: bodyRowClassName:
'border-b border-gray-100 hover:bg-gray-50 transition-colors', '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', paginationClassName: 'hidden',
}} }}
/> />