feat(FE-218,212,213): implement PurchaseOrderDetail component and update related types

This commit is contained in:
rstubryan
2025-11-18 14:30:09 +07:00
parent edd59598f9
commit e8dd4f3759
5 changed files with 102 additions and 388 deletions
+6 -4
View File
@@ -2,7 +2,7 @@
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import PurchaseRequestForm from '@/components/pages/purchase/form/request/PurchaseRequestForm';
import PurchaseOrderDetail from '@/components/pages/purchase/order/PurchaseOrderDetail';
import { PurchaseRequestApi } from '@/services/api/purchase';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
@@ -33,12 +33,14 @@ const PurchaseDetail = () => {
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
<div className='w-full p-4'>
{isLoadingPurchase && (
<span className='loading loading-spinner loading-xl' />
<div className='w-full flex flex-row justify-center items-center'>
<span className='loading loading-spinner loading-xl' />
</div>
)}
{!isLoadingPurchase && isResponseSuccess(purchase) && (
<PurchaseRequestForm type='detail' initialValues={purchase.data} />
<PurchaseOrderDetail type='detail' data={purchase.data} />
)}
</div>
);
@@ -206,15 +206,13 @@ const PurchaseOrderAcceptApprovalForm = ({
if (initialValues?.items) {
return initialValues.items.map((item) => ({
value: item.id,
label: `${item.product.name} (${item.quantity} ${item.product.uom.name})`,
label: `${item.product.name} (${item.quantity} ${item.product.uom?.name || 'unit'})`,
id: item.id,
quantity: item.quantity,
product: {
name: item.product.name,
product_category: item.product.product_category,
uom: {
name: item.product.uom.name,
},
uom: item.product.uom,
},
warehouse: {
name: item.warehouse?.name || '',
@@ -44,7 +44,7 @@ const PurchaseOrderStaffApprovalForm = ({
product: {
name: string;
type?: string;
uom: {
uom?: {
name: string;
};
};
@@ -170,15 +170,13 @@ const PurchaseOrderStaffApprovalForm = ({
if (initialValues?.items) {
return initialValues.items.map((item) => ({
value: item.id,
label: `${item.product.name} (${item.quantity} ${item.product.uom.name})`,
label: `${item.product.name} (${item.quantity} ${item.product.uom?.name || 'unit'})`,
id: item.id,
quantity: item.quantity,
product: {
name: item.product.name,
product_category: item.product.product_category,
uom: {
name: item.product.uom.name,
},
uom: item.product.uom,
},
warehouse: {
name: item.warehouse?.name || '',
@@ -3,9 +3,7 @@
import { useCallback, useMemo, useState } from 'react';
import { ColumnDef, SortingState } from '@tanstack/react-table';
import ApprovalSteps, {
formatGroupedApprovalsToApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import ApprovalSteps from '@/components/pages/ApprovalSteps';
import Table from '@/components/Table';
import Button from '@/components/Button';
import { Icon } from '@iconify/react';
@@ -20,21 +18,13 @@ import PurchaseOrderStaffApprovalForm from '@/components/pages/purchase/form/ord
import PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm';
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
import { BaseGroupedApproval } from '@/types/api/api-general';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
import { ApprovalStepStatus } from '@/components/pages/ApprovalSteps';
import Card from '@/components/Card';
import {
CreateManagerApprovalRequestPayload,
Purchase,
PurchaseItem,
} from '@/types/api/purchase/purchase';
import {
createdUser,
dummyAreas,
dummyLocations,
dummyProductWarehouses,
dummyWarehouses,
} from '@/dummy/marketing.dummy';
import {
ManagerApprovalApi,
PurchaseDeleteItemsApi,
@@ -81,321 +71,8 @@ interface PurchaseOrderDetailProps {
data?: Purchase;
}
const dummyPurchaseData: Purchase = {
id: 1,
pr_number: 'PR-MBU-01837',
po_number: 'PO-MBU-01837',
po_document_path: '/documents/po-mbu-01837.pdf',
po_date: '2025-01-10T00:00:00Z',
supplier: {
id: 1,
name: 'PT. CHAROEN POKPHAND JAYA FARM',
alias: 'CP JAYA FARM',
pic: 'Budi Santoso',
type: 'Supplier',
category: 'Feed',
hatchery: 'Jawa Barat',
phone: '+62-22-7563850',
email: 'info@cp.co.id',
address:
'Jl. Raya Bandung - Sumedang Km. 28, Desa Cisantana, Kec. Cigendel, Kabupaten Sumedang, Jawa Barat 45363',
npwp: '01.938.451.6-433.000',
account_number: '123-456-7890',
due_date: 30,
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],
},
{
id: 2,
purchase_id: 2,
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 dummyGoodsReceiptItems: PurchaseItem[] = [
{
id: 1,
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,
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],
},
];
const dummyGroupedApprovals: BaseGroupedApproval[] = [
{
step_number: 1,
step_name: 'Pengajuan',
approvals: [
{
step_number: 1,
step_name: 'Pengajuan',
action: 'submit',
notes: 'Pengajuan purchase order dibuat',
action_by: {
id: 1,
id_user: 1,
email: 'user@company.com',
name: 'User Pengajuan',
},
action_at: '2025-01-10T08:00:00Z',
},
],
},
{
step_number: 2,
step_name: 'Approval Purchasing',
approvals: [
{
step_number: 2,
step_name: 'Approval Purchasing',
action: 'approve',
notes: 'Purchase order disetujui oleh purchasing',
action_by: {
id: 2,
id_user: 2,
email: 'purchasing@company.com',
name: 'Staff Purchasing',
},
action_at: '2025-01-10T10:30:00Z',
},
],
},
{
step_number: 3,
step_name: 'Approval Manager Purchasing',
approvals: [
{
step_number: 3,
step_name: 'Approval Manager Purchasing',
action: 'approve',
notes: 'Purchase order disetujui oleh manager purchasing',
action_by: {
id: 3,
id_user: 3,
email: 'manager.purchasing@company.com',
name: 'Manager Purchasing',
},
action_at: '2025-01-10T14:15:00Z',
},
],
},
{
step_number: 4,
step_name: 'Produk Diterima',
approvals: [
{
step_number: 4,
step_name: 'Produk Diterima',
action: 'receive',
notes: 'Produk telah diterima sesuai pesanan',
action_by: {
id: 4,
id_user: 4,
email: 'user@company.com',
name: 'User Pengajuan',
},
action_at: '2025-01-12T09:00:00Z',
},
],
},
{
step_number: 5,
step_name: 'Selesai',
approvals: [
{
step_number: 5,
step_name: 'Selesai',
action: 'complete',
notes: 'Purchase order telah selesai diproses',
action_by: {
id: 5,
id_user: 5,
email: 'user@company.com',
name: 'User Pengajuan',
},
action_at: '2025-01-12T16:00:00Z',
},
],
},
];
const PurchaseOrderDetail = ({
type = 'detail',
@@ -420,23 +97,49 @@ const PurchaseOrderDetail = ({
parseInt(item)
);
// ===== STATIC DATA =====
const purchaseData = data || dummyPurchaseData;
// ===== COMPUTED VALUES =====
const purchaseOrderItems = useMemo(
() => purchaseData.items || [],
[purchaseData.items]
() => data?.items || [],
[data?.items]
);
const goodsReceiptItems = dummyGoodsReceiptItems;
const groupedApprovals = dummyGroupedApprovals;
const latestApproval =
groupedApprovals[groupedApprovals.length - 1]?.approvals[0];
// Create goods receipt items from received items
const goodsReceiptItems = useMemo(() => {
return purchaseOrderItems.filter(item => item.received_date);
}, [purchaseOrderItems]);
// Create simple approval steps from single approval data
const approvalSteps = useMemo(() => {
if (!data?.approval) return [];
// Create a simple approval step based on the single approval data
const status = data.approval.action === 'APPROVED' ? 'APPROVED' :
data.approval.action === 'REJECTED' ? 'REJECTED' : 'WAITING';
return [{
name: data.approval.step_name,
status: status as ApprovalStepStatus,
logs: [{
action_by: data.approval.action_by?.name,
date: data.approval.action_at,
notes: data.approval.notes,
}],
}];
}, [data?.approval]);
const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
}, [purchaseOrderItems]);
// ===== SUBMISSION HANDLER =====
const createManagerApprovalHandler = useCallback(
async (payload: CreateManagerApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: purchaseData?.id || 1;
: data?.id || 1;
if (!purchaseRequestId) {
toast.error('Purchase Request ID is required');
@@ -454,14 +157,14 @@ const PurchaseOrderDetail = ({
}
toast.success(res?.message as string);
},
[purchaseData?.id, searchParams]
[data?.id, searchParams]
);
// ===== DELETE HANDLER =====
const deleteItemsHandler = useCallback(async () => {
const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!)
: purchaseData?.id || 1;
: data?.id || 1;
if (!purchaseRequestId) {
toast.error('Purchase Request ID is required');
@@ -501,30 +204,14 @@ const PurchaseOrderDetail = ({
} finally {
setIsDeleteLoading(false);
}
}, [purchaseData?.id, searchParams, selectedItem, selectedRowIds]);
}, [data?.id, searchParams, selectedItem, selectedRowIds]);
// ===== COMPUTED VALUES =====
const approvalSteps = useMemo(() => {
if (!groupedApprovals.length || !latestApproval) return [];
// Return null if no data provided
if (!data) {
return null;
}
try {
return formatGroupedApprovalsToApprovalSteps(
PURCHASE_ORDER_APPROVAL_LINE,
groupedApprovals,
latestApproval
);
} catch (error) {
console.error('Error formatting approval steps:', error);
return [];
}
}, [groupedApprovals, latestApproval]);
const totalBeforeTax = useMemo(() => {
return purchaseOrderItems.reduce(
(sum, item) => sum + (item.total_price || 0),
0
);
}, [purchaseOrderItems]);
const purchaseData = data;
const purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [
{
@@ -563,10 +250,15 @@ const PurchaseOrderDetail = ({
cell: (props) => props.row.original.product?.name || '-',
},
{
accessorKey: 'product.product_category.name',
accessorKey: 'product.product_category',
header: 'Jenis Produk',
cell: (props) =>
props.row.original.product?.product_category?.name || '-',
cell: (props) => {
const category = props.row.original.product?.product_category;
if (typeof category === 'string') {
return category;
}
return category?.name || '-';
},
},
{
accessorKey: 'quantity',
@@ -576,7 +268,13 @@ const PurchaseOrderDetail = ({
{
accessorKey: 'product.uom.name',
header: 'Satuan',
cell: (props) => props.row.original.product?.uom?.name || '-',
cell: (props) => {
const uom = props.row.original.product?.uom;
if (uom && typeof uom === 'object' && uom.name) {
return uom.name;
}
return uom || '-';
},
},
{
accessorKey: 'price',
@@ -628,10 +326,12 @@ const PurchaseOrderDetail = ({
: '-',
},
{
accessorKey: 'product_warehouse.warehouse.name',
accessorKey: 'warehouse.name',
header: 'Gudang Tujuan',
cell: (props) =>
props.row.original.product_warehouse?.warehouse?.name || '-',
cell: (props) => {
const warehouse = props.row.original.warehouse;
return warehouse?.name || '-';
},
},
{
accessorKey: 'travel_number',
@@ -639,10 +339,10 @@ const PurchaseOrderDetail = ({
cell: (props) => props.row.original.travel_number || '-',
},
{
accessorKey: 'travel_number_docs',
accessorKey: 'travel_document_path',
header: 'Dokumen Surat Jalan',
cell: (props) => {
const documentPath = props.row.original.travel_number_docs;
const documentPath = props.row.original.travel_document_path;
return documentPath ? (
<Button
color='primary'
@@ -807,7 +507,7 @@ const PurchaseOrderDetail = ({
Area
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.area?.name || '-'}
: {purchaseData.items?.[0]?.warehouse?.area?.name || '-'}
</span>
</div>
</div>
@@ -817,7 +517,7 @@ const PurchaseOrderDetail = ({
Lokasi
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.location?.name || '-'}
: {purchaseData.items?.[0]?.warehouse?.type !== 'AREA' && purchaseData.items?.[0]?.warehouse?.location?.name ? purchaseData.items[0].warehouse.location.name : '-'}
</span>
</div>
</div>
@@ -827,7 +527,7 @@ const PurchaseOrderDetail = ({
Gudang
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.warehouse?.name || '-'}
: {purchaseData.items?.[0]?.warehouse?.name || '-'}
</span>
</div>
</div>
@@ -848,10 +548,10 @@ const PurchaseOrderDetail = ({
<div className='group'>
<div className='flex items-start'>
<span className='font-medium text-gray-600 min-w-[140px] flex-shrink-0'>
Alamat Supplier
Kategori Supplier
</span>
<span className='text-gray-900 ml-3 break-all'>
: {purchaseData.supplier?.address || '-'}
: {purchaseData.supplier?.category || '-'}
</span>
</div>
</div>
+19 -3
View File
@@ -1,4 +1,4 @@
import { BaseMetadata } from '@/types/api/api-general';
import { BaseApproval, BaseMetadata } from '@/types/api/api-general';
import { Supplier } from '@/types/api/master-data/supplier';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { Product } from '@/types/api/master-data/product';
@@ -6,11 +6,25 @@ import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { Area } from '@/types/api/master-data/area';
import { Location } from '@/types/api/master-data/location';
export type PurchaseItemProduct = {
id: number;
name: string;
flags?: string[];
uom?: {
name: string;
};
product_category?:
| {
name: string;
}
| string;
};
export type PurchaseItem = {
id: number;
purchase_id?: number;
product_id: number;
warehouse: Warehouse;
product: Product;
product: PurchaseItemProduct | Product;
product_warehouse: ProductWarehouse;
quantity: number;
qty: number;
@@ -22,6 +36,7 @@ export type PurchaseItem = {
received_date?: string | null;
travel_number?: string | null;
travel_number_docs?: string | null;
travel_document_path?: string | null;
vehicle_number?: string | null;
};
@@ -42,6 +57,7 @@ export type BasePurchase = {
location?: Location;
warehouse?: Warehouse;
items?: PurchaseItem[];
approval?: BaseApproval;
};
export type Purchase = BaseMetadata & BasePurchase;