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 { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr'; 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 { PurchaseRequestApi } from '@/services/api/purchase';
import { isResponseSuccess, isResponseError } from '@/lib/api-helper'; import { isResponseSuccess, isResponseError } from '@/lib/api-helper';
@@ -33,12 +33,14 @@ const PurchaseDetail = () => {
} }
return ( return (
<div className='w-full p-4 flex flex-row justify-center'> <div className='w-full p-4'>
{isLoadingPurchase && ( {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) && ( {!isLoadingPurchase && isResponseSuccess(purchase) && (
<PurchaseRequestForm type='detail' initialValues={purchase.data} /> <PurchaseOrderDetail type='detail' data={purchase.data} />
)} )}
</div> </div>
); );
@@ -206,15 +206,13 @@ const PurchaseOrderAcceptApprovalForm = ({
if (initialValues?.items) { if (initialValues?.items) {
return initialValues.items.map((item) => ({ return initialValues.items.map((item) => ({
value: item.id, 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, id: item.id,
quantity: item.quantity, quantity: item.quantity,
product: { product: {
name: item.product.name, name: item.product.name,
product_category: item.product.product_category, product_category: item.product.product_category,
uom: { uom: item.product.uom,
name: item.product.uom.name,
},
}, },
warehouse: { warehouse: {
name: item.warehouse?.name || '', name: item.warehouse?.name || '',
@@ -44,7 +44,7 @@ const PurchaseOrderStaffApprovalForm = ({
product: { product: {
name: string; name: string;
type?: string; type?: string;
uom: { uom?: {
name: string; name: string;
}; };
}; };
@@ -170,15 +170,13 @@ const PurchaseOrderStaffApprovalForm = ({
if (initialValues?.items) { if (initialValues?.items) {
return initialValues.items.map((item) => ({ return initialValues.items.map((item) => ({
value: item.id, 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, id: item.id,
quantity: item.quantity, quantity: item.quantity,
product: { product: {
name: item.product.name, name: item.product.name,
product_category: item.product.product_category, product_category: item.product.product_category,
uom: { uom: item.product.uom,
name: item.product.uom.name,
},
}, },
warehouse: { warehouse: {
name: item.warehouse?.name || '', name: item.warehouse?.name || '',
@@ -3,9 +3,7 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { ColumnDef, SortingState } from '@tanstack/react-table'; import { ColumnDef, SortingState } from '@tanstack/react-table';
import ApprovalSteps, { import ApprovalSteps from '@/components/pages/ApprovalSteps';
formatGroupedApprovalsToApprovalSteps,
} from '@/components/pages/ApprovalSteps';
import Table from '@/components/Table'; import Table from '@/components/Table';
import Button from '@/components/Button'; import Button from '@/components/Button';
import { Icon } from '@iconify/react'; 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 PurchaseOrderAcceptApprovalForm from '@/components/pages/purchase/form/order/PurchaseOrderAcceptApprovalForm';
import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice'; import PurchaseOrderInvoice from '@/components/pages/purchase/order/PurchaseOrderInvoice';
import { BaseGroupedApproval } from '@/types/api/api-general'; import { ApprovalStepStatus } from '@/components/pages/ApprovalSteps';
import { PURCHASE_ORDER_APPROVAL_LINE } from '@/config/approval-line';
import Card from '@/components/Card'; import Card from '@/components/Card';
import { import {
CreateManagerApprovalRequestPayload, CreateManagerApprovalRequestPayload,
Purchase, Purchase,
PurchaseItem, PurchaseItem,
} from '@/types/api/purchase/purchase'; } from '@/types/api/purchase/purchase';
import {
createdUser,
dummyAreas,
dummyLocations,
dummyProductWarehouses,
dummyWarehouses,
} from '@/dummy/marketing.dummy';
import { import {
ManagerApprovalApi, ManagerApprovalApi,
PurchaseDeleteItemsApi, PurchaseDeleteItemsApi,
@@ -81,321 +71,8 @@ interface PurchaseOrderDetailProps {
data?: Purchase; 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 = ({ const PurchaseOrderDetail = ({
type = 'detail', type = 'detail',
@@ -420,23 +97,49 @@ const PurchaseOrderDetail = ({
parseInt(item) parseInt(item)
); );
// ===== STATIC DATA ===== // ===== COMPUTED VALUES =====
const purchaseData = data || dummyPurchaseData;
const purchaseOrderItems = useMemo( const purchaseOrderItems = useMemo(
() => purchaseData.items || [], () => data?.items || [],
[purchaseData.items] [data?.items]
); );
const goodsReceiptItems = dummyGoodsReceiptItems;
const groupedApprovals = dummyGroupedApprovals; // Create goods receipt items from received items
const latestApproval = const goodsReceiptItems = useMemo(() => {
groupedApprovals[groupedApprovals.length - 1]?.approvals[0]; 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 ===== // ===== SUBMISSION HANDLER =====
const createManagerApprovalHandler = useCallback( const createManagerApprovalHandler = useCallback(
async (payload: CreateManagerApprovalRequestPayload) => { async (payload: CreateManagerApprovalRequestPayload) => {
const purchaseRequestId = searchParams.get('purchaseId') const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!) ? parseInt(searchParams.get('purchaseId')!)
: purchaseData?.id || 1; : data?.id || 1;
if (!purchaseRequestId) { if (!purchaseRequestId) {
toast.error('Purchase Request ID is required'); toast.error('Purchase Request ID is required');
@@ -454,14 +157,14 @@ const PurchaseOrderDetail = ({
} }
toast.success(res?.message as string); toast.success(res?.message as string);
}, },
[purchaseData?.id, searchParams] [data?.id, searchParams]
); );
// ===== DELETE HANDLER ===== // ===== DELETE HANDLER =====
const deleteItemsHandler = useCallback(async () => { const deleteItemsHandler = useCallback(async () => {
const purchaseRequestId = searchParams.get('purchaseId') const purchaseRequestId = searchParams.get('purchaseId')
? parseInt(searchParams.get('purchaseId')!) ? parseInt(searchParams.get('purchaseId')!)
: purchaseData?.id || 1; : data?.id || 1;
if (!purchaseRequestId) { if (!purchaseRequestId) {
toast.error('Purchase Request ID is required'); toast.error('Purchase Request ID is required');
@@ -501,30 +204,14 @@ const PurchaseOrderDetail = ({
} finally { } finally {
setIsDeleteLoading(false); setIsDeleteLoading(false);
} }
}, [purchaseData?.id, searchParams, selectedItem, selectedRowIds]); }, [data?.id, searchParams, selectedItem, selectedRowIds]);
// ===== COMPUTED VALUES ===== // Return null if no data provided
const approvalSteps = useMemo(() => { if (!data) {
if (!groupedApprovals.length || !latestApproval) return []; return null;
}
try { const purchaseData = data;
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 purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [ const purchaseOrderColumns: ColumnDef<PurchaseItem>[] = [
{ {
@@ -563,10 +250,15 @@ const PurchaseOrderDetail = ({
cell: (props) => props.row.original.product?.name || '-', cell: (props) => props.row.original.product?.name || '-',
}, },
{ {
accessorKey: 'product.product_category.name', accessorKey: 'product.product_category',
header: 'Jenis Produk', header: 'Jenis Produk',
cell: (props) => cell: (props) => {
props.row.original.product?.product_category?.name || '-', const category = props.row.original.product?.product_category;
if (typeof category === 'string') {
return category;
}
return category?.name || '-';
},
}, },
{ {
accessorKey: 'quantity', accessorKey: 'quantity',
@@ -576,7 +268,13 @@ const PurchaseOrderDetail = ({
{ {
accessorKey: 'product.uom.name', accessorKey: 'product.uom.name',
header: 'Satuan', 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', accessorKey: 'price',
@@ -628,10 +326,12 @@ const PurchaseOrderDetail = ({
: '-', : '-',
}, },
{ {
accessorKey: 'product_warehouse.warehouse.name', accessorKey: 'warehouse.name',
header: 'Gudang Tujuan', header: 'Gudang Tujuan',
cell: (props) => cell: (props) => {
props.row.original.product_warehouse?.warehouse?.name || '-', const warehouse = props.row.original.warehouse;
return warehouse?.name || '-';
},
}, },
{ {
accessorKey: 'travel_number', accessorKey: 'travel_number',
@@ -639,10 +339,10 @@ const PurchaseOrderDetail = ({
cell: (props) => props.row.original.travel_number || '-', cell: (props) => props.row.original.travel_number || '-',
}, },
{ {
accessorKey: 'travel_number_docs', accessorKey: 'travel_document_path',
header: 'Dokumen Surat Jalan', header: 'Dokumen Surat Jalan',
cell: (props) => { cell: (props) => {
const documentPath = props.row.original.travel_number_docs; const documentPath = props.row.original.travel_document_path;
return documentPath ? ( return documentPath ? (
<Button <Button
color='primary' color='primary'
@@ -807,7 +507,7 @@ const PurchaseOrderDetail = ({
Area Area
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
: {purchaseData.area?.name || '-'} : {purchaseData.items?.[0]?.warehouse?.area?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -817,7 +517,7 @@ const PurchaseOrderDetail = ({
Lokasi Lokasi
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <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> </span>
</div> </div>
</div> </div>
@@ -827,7 +527,7 @@ const PurchaseOrderDetail = ({
Gudang Gudang
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
: {purchaseData.warehouse?.name || '-'} : {purchaseData.items?.[0]?.warehouse?.name || '-'}
</span> </span>
</div> </div>
</div> </div>
@@ -848,10 +548,10 @@ const PurchaseOrderDetail = ({
<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 Supplier Kategori Supplier
</span> </span>
<span className='text-gray-900 ml-3 break-all'> <span className='text-gray-900 ml-3 break-all'>
: {purchaseData.supplier?.address || '-'} : {purchaseData.supplier?.category || '-'}
</span> </span>
</div> </div>
</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 { Supplier } from '@/types/api/master-data/supplier';
import { Warehouse } from '@/types/api/master-data/warehouse'; import { Warehouse } from '@/types/api/master-data/warehouse';
import { Product } from '@/types/api/master-data/product'; 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 { Area } from '@/types/api/master-data/area';
import { Location } from '@/types/api/master-data/location'; 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 = { export type PurchaseItem = {
id: number; id: number;
purchase_id?: number; product_id: number;
warehouse: Warehouse; warehouse: Warehouse;
product: Product; product: PurchaseItemProduct | Product;
product_warehouse: ProductWarehouse; product_warehouse: ProductWarehouse;
quantity: number; quantity: number;
qty: number; qty: number;
@@ -22,6 +36,7 @@ export type PurchaseItem = {
received_date?: string | null; received_date?: string | null;
travel_number?: string | null; travel_number?: string | null;
travel_number_docs?: string | null; travel_number_docs?: string | null;
travel_document_path?: string | null;
vehicle_number?: string | null; vehicle_number?: string | null;
}; };
@@ -42,6 +57,7 @@ export type BasePurchase = {
location?: Location; location?: Location;
warehouse?: Warehouse; warehouse?: Warehouse;
items?: PurchaseItem[]; items?: PurchaseItem[];
approval?: BaseApproval;
}; };
export type Purchase = BaseMetadata & BasePurchase; export type Purchase = BaseMetadata & BasePurchase;