693 lines
22 KiB
TypeScript
693 lines
22 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
Form,
|
||
|
|
Input,
|
||
|
|
Select,
|
||
|
|
Button,
|
||
|
|
Table,
|
||
|
|
Modal,
|
||
|
|
Descriptions,
|
||
|
|
Divider,
|
||
|
|
message,
|
||
|
|
Space,
|
||
|
|
Tag,
|
||
|
|
Row,
|
||
|
|
Col,
|
||
|
|
InputNumber,
|
||
|
|
Steps,
|
||
|
|
Upload,
|
||
|
|
Tooltip,
|
||
|
|
Badge,
|
||
|
|
Statistic,
|
||
|
|
Progress,
|
||
|
|
Alert,
|
||
|
|
} from 'antd';
|
||
|
|
import {
|
||
|
|
ShoppingCartOutlined,
|
||
|
|
UploadOutlined,
|
||
|
|
CheckCircleOutlined,
|
||
|
|
CloseCircleOutlined,
|
||
|
|
SyncOutlined,
|
||
|
|
DownloadOutlined,
|
||
|
|
FileExcelOutlined,
|
||
|
|
UserOutlined,
|
||
|
|
DeleteOutlined,
|
||
|
|
EyeOutlined,
|
||
|
|
} from '@ant-design/icons';
|
||
|
|
import type { ColumnsType } from 'antd/es/table';
|
||
|
|
|
||
|
|
const { Option } = Select;
|
||
|
|
const { TextArea } = Input;
|
||
|
|
|
||
|
|
interface BatchOrderItem {
|
||
|
|
key: string;
|
||
|
|
lineNumber: number;
|
||
|
|
productId: string;
|
||
|
|
sku: string;
|
||
|
|
productName: string;
|
||
|
|
quantity: number;
|
||
|
|
unitPrice: number;
|
||
|
|
totalPrice: number;
|
||
|
|
customerId: string;
|
||
|
|
customerName: string;
|
||
|
|
status: 'VALID' | 'INVALID' | 'PENDING';
|
||
|
|
error?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface BatchOrder {
|
||
|
|
id: string;
|
||
|
|
batchId: string;
|
||
|
|
customerId: string;
|
||
|
|
customerName: string;
|
||
|
|
totalItems: number;
|
||
|
|
totalAmount: number;
|
||
|
|
currency: string;
|
||
|
|
status: 'DRAFT' | 'PENDING_REVIEW' | 'CONFIRMED' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED';
|
||
|
|
createdAt: string;
|
||
|
|
validItems: number;
|
||
|
|
invalidItems: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Customer {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
company: string;
|
||
|
|
tier: 'BASIC' | 'PRO' | 'ENTERPRISE';
|
||
|
|
}
|
||
|
|
|
||
|
|
const BATCH_STATUS_MAP: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||
|
|
DRAFT: { color: 'default', text: 'Draft', icon: <FileExcelOutlined /> },
|
||
|
|
PENDING_REVIEW: { color: 'processing', text: 'Pending Review', icon: <SyncOutlined spin /> },
|
||
|
|
CONFIRMED: { color: 'blue', text: 'Confirmed', icon: <CheckCircleOutlined /> },
|
||
|
|
PROCESSING: { color: 'blue', text: 'Processing', icon: <SyncOutlined spin /> },
|
||
|
|
COMPLETED: { color: 'success', text: 'Completed', icon: <CheckCircleOutlined /> },
|
||
|
|
CANCELLED: { color: 'error', text: 'Cancelled', icon: <CloseCircleOutlined /> },
|
||
|
|
};
|
||
|
|
|
||
|
|
const ITEM_STATUS_MAP: Record<string, { color: string; text: string }> = {
|
||
|
|
VALID: { color: 'success', text: 'Valid' },
|
||
|
|
INVALID: { color: 'error', text: 'Invalid' },
|
||
|
|
PENDING: { color: 'warning', text: 'Pending' },
|
||
|
|
};
|
||
|
|
|
||
|
|
export const BatchOrder: React.FC = () => {
|
||
|
|
const [form] = Form.useForm();
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [batchOrders, setBatchOrders] = useState<BatchOrder[]>([]);
|
||
|
|
const [customers, setCustomers] = useState<Customer[]>([]);
|
||
|
|
const [orderItems, setOrderItems] = useState<BatchOrderItem[]>([]);
|
||
|
|
const [currentStep, setCurrentStep] = useState(0);
|
||
|
|
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||
|
|
const [selectedBatch, setSelectedBatch] = useState<BatchOrder | null>(null);
|
||
|
|
const [selectedCustomer, setSelectedCustomer] = useState<Customer | null>(null);
|
||
|
|
const [fileList, setFileList] = useState<any[]>([]);
|
||
|
|
const [stats, setStats] = useState({
|
||
|
|
total: 0,
|
||
|
|
pending: 0,
|
||
|
|
processing: 0,
|
||
|
|
completed: 0,
|
||
|
|
totalAmount: 0,
|
||
|
|
});
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchBatchOrders();
|
||
|
|
fetchCustomers();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const fetchBatchOrders = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const mockOrders: BatchOrder[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
batchId: 'BO-2026-001',
|
||
|
|
customerId: 'CUST_001',
|
||
|
|
customerName: 'ABC Trading Co.',
|
||
|
|
totalItems: 50,
|
||
|
|
totalAmount: 25000.00,
|
||
|
|
currency: 'USD',
|
||
|
|
status: 'CONFIRMED',
|
||
|
|
createdAt: '2026-03-18 10:00:00',
|
||
|
|
validItems: 48,
|
||
|
|
invalidItems: 2,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
batchId: 'BO-2026-002',
|
||
|
|
customerId: 'CUST_002',
|
||
|
|
customerName: 'XYZ Electronics Ltd.',
|
||
|
|
totalItems: 100,
|
||
|
|
totalAmount: 45000.00,
|
||
|
|
currency: 'USD',
|
||
|
|
status: 'PENDING_REVIEW',
|
||
|
|
createdAt: '2026-03-17 14:30:00',
|
||
|
|
validItems: 95,
|
||
|
|
invalidItems: 5,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
batchId: 'BO-2026-003',
|
||
|
|
customerId: 'CUST_003',
|
||
|
|
customerName: 'Global Import Inc.',
|
||
|
|
totalItems: 200,
|
||
|
|
totalAmount: 85000.00,
|
||
|
|
currency: 'USD',
|
||
|
|
status: 'COMPLETED',
|
||
|
|
createdAt: '2026-03-15 09:00:00',
|
||
|
|
validItems: 200,
|
||
|
|
invalidItems: 0,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
setBatchOrders(mockOrders);
|
||
|
|
calculateStats(mockOrders);
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Failed to load batch orders');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const fetchCustomers = async () => {
|
||
|
|
const mockCustomers: Customer[] = [
|
||
|
|
{ id: 'CUST_001', name: 'ABC Trading Co.', company: 'ABC Trading', tier: 'ENTERPRISE' },
|
||
|
|
{ id: 'CUST_002', name: 'XYZ Electronics Ltd.', company: 'XYZ Electronics', tier: 'PRO' },
|
||
|
|
{ id: 'CUST_003', name: 'Global Import Inc.', company: 'Global Import', tier: 'ENTERPRISE' },
|
||
|
|
];
|
||
|
|
setCustomers(mockCustomers);
|
||
|
|
};
|
||
|
|
|
||
|
|
const calculateStats = (orders: BatchOrder[]) => {
|
||
|
|
setStats({
|
||
|
|
total: orders.length,
|
||
|
|
pending: orders.filter((o) => o.status === 'PENDING_REVIEW').length,
|
||
|
|
processing: orders.filter((o) => o.status === 'PROCESSING').length,
|
||
|
|
completed: orders.filter((o) => o.status === 'COMPLETED').length,
|
||
|
|
totalAmount: orders.filter((o) => o.status === 'COMPLETED').reduce((sum, o) => sum + o.totalAmount, 0),
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleCustomerChange = (customerId: string) => {
|
||
|
|
const customer = customers.find((c) => c.id === customerId);
|
||
|
|
setSelectedCustomer(customer || null);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleFileUpload = (info: any) => {
|
||
|
|
setFileList(info.fileList.slice(-1));
|
||
|
|
if (info.file.status === 'done' || info.file) {
|
||
|
|
parseUploadedFile(info.file);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const parseUploadedFile = (file: any) => {
|
||
|
|
const mockItems: BatchOrderItem[] = [
|
||
|
|
{ key: '1', lineNumber: 1, productId: 'PROD_001', sku: 'SKU-001', productName: 'Wireless Headphones', quantity: 100, unitPrice: 22.00, totalPrice: 2200.00, customerId: selectedCustomer?.id || '', customerName: selectedCustomer?.name || '', status: 'VALID' },
|
||
|
|
{ key: '2', lineNumber: 2, productId: 'PROD_002', sku: 'SKU-002', productName: 'USB-C Cable', quantity: 500, unitPrice: 4.50, totalPrice: 2250.00, customerId: selectedCustomer?.id || '', customerName: selectedCustomer?.name || '', status: 'VALID' },
|
||
|
|
{ key: '3', lineNumber: 3, productId: 'PROD_003', sku: 'SKU-003', productName: 'Phone Case', quantity: 200, unitPrice: 7.00, totalPrice: 1400.00, customerId: selectedCustomer?.id || '', customerName: selectedCustomer?.name || '', status: 'VALID' },
|
||
|
|
{ key: '4', lineNumber: 4, productId: 'PROD_004', sku: 'SKU-004', productName: 'Invalid Product', quantity: 50, unitPrice: 0, totalPrice: 0, customerId: selectedCustomer?.id || '', customerName: selectedCustomer?.name || '', status: 'INVALID', error: 'Product not found' },
|
||
|
|
];
|
||
|
|
setOrderItems(mockItems);
|
||
|
|
setCurrentStep(1);
|
||
|
|
message.success('File parsed successfully');
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleRemoveItem = (key: string) => {
|
||
|
|
setOrderItems(orderItems.filter((item) => item.key !== key));
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleValidateItems = () => {
|
||
|
|
setLoading(true);
|
||
|
|
setTimeout(() => {
|
||
|
|
const validCount = orderItems.filter((i) => i.status === 'VALID').length;
|
||
|
|
const invalidCount = orderItems.filter((i) => i.status === 'INVALID').length;
|
||
|
|
message.success(`Validation complete: ${validCount} valid, ${invalidCount} invalid`);
|
||
|
|
setLoading(false);
|
||
|
|
}, 1000);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmitBatch = async () => {
|
||
|
|
try {
|
||
|
|
const values = await form.validateFields();
|
||
|
|
const validItems = orderItems.filter((i) => i.status === 'VALID');
|
||
|
|
if (validItems.length === 0) {
|
||
|
|
message.error('No valid items to submit');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setLoading(true);
|
||
|
|
const totalAmount = validItems.reduce((sum, item) => sum + item.totalPrice, 0);
|
||
|
|
|
||
|
|
const batchData = {
|
||
|
|
customerId: values.customerId,
|
||
|
|
items: validItems,
|
||
|
|
totalAmount,
|
||
|
|
note: values.note,
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log('Submitting batch order:', batchData);
|
||
|
|
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||
|
|
|
||
|
|
message.success('Batch order submitted successfully');
|
||
|
|
setCurrentStep(2);
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Failed to submit batch order');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleViewDetail = (batch: BatchOrder) => {
|
||
|
|
setSelectedBatch(batch);
|
||
|
|
setDetailModalVisible(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleApproveBatch = async (batchId: string) => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
console.log('Approving batch order:', batchId);
|
||
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
|
message.success('Batch order approved');
|
||
|
|
fetchBatchOrders();
|
||
|
|
} catch (error) {
|
||
|
|
message.error('Failed to approve batch order');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDownloadTemplate = () => {
|
||
|
|
message.info('Downloading template...');
|
||
|
|
};
|
||
|
|
|
||
|
|
const itemColumns: ColumnsType<BatchOrderItem> = [
|
||
|
|
{ title: 'Line', dataIndex: 'lineNumber', key: 'lineNumber', width: 60 },
|
||
|
|
{ title: 'SKU', dataIndex: 'sku', key: 'sku', width: 100 },
|
||
|
|
{ title: 'Product', dataIndex: 'productName', key: 'productName' },
|
||
|
|
{ title: 'Qty', dataIndex: 'quantity', key: 'quantity', width: 80 },
|
||
|
|
{
|
||
|
|
title: 'Unit Price',
|
||
|
|
dataIndex: 'unitPrice',
|
||
|
|
key: 'unitPrice',
|
||
|
|
width: 100,
|
||
|
|
render: (price: number) => `$${price.toFixed(2)}`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Total',
|
||
|
|
dataIndex: 'totalPrice',
|
||
|
|
key: 'totalPrice',
|
||
|
|
width: 100,
|
||
|
|
render: (price: number) => `$${price.toFixed(2)}`,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Status',
|
||
|
|
dataIndex: 'status',
|
||
|
|
key: 'status',
|
||
|
|
width: 90,
|
||
|
|
render: (status: string) => {
|
||
|
|
const config = ITEM_STATUS_MAP[status];
|
||
|
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Error',
|
||
|
|
dataIndex: 'error',
|
||
|
|
key: 'error',
|
||
|
|
width: 150,
|
||
|
|
render: (error?: string) => error ? <span style={{ color: '#f5222d' }}>{error}</span> : '-',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Action',
|
||
|
|
key: 'action',
|
||
|
|
width: 80,
|
||
|
|
render: (_, record) => (
|
||
|
|
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleRemoveItem(record.key)} />
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const batchColumns: ColumnsType<BatchOrder> = [
|
||
|
|
{
|
||
|
|
title: 'Batch ID',
|
||
|
|
dataIndex: 'batchId',
|
||
|
|
key: 'batchId',
|
||
|
|
width: 130,
|
||
|
|
render: (text: string, record) => (
|
||
|
|
<a onClick={() => handleViewDetail(record)}>{text}</a>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Customer',
|
||
|
|
dataIndex: 'customerName',
|
||
|
|
key: 'customerName',
|
||
|
|
width: 180,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Status',
|
||
|
|
dataIndex: 'status',
|
||
|
|
key: 'status',
|
||
|
|
width: 130,
|
||
|
|
render: (status: string) => {
|
||
|
|
const config = BATCH_STATUS_MAP[status];
|
||
|
|
return <Tag icon={config.icon} color={config.color}>{config.text}</Tag>;
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Items',
|
||
|
|
key: 'items',
|
||
|
|
width: 120,
|
||
|
|
render: (_, record) => (
|
||
|
|
<Space>
|
||
|
|
<Badge count={record.validItems} style={{ backgroundColor: '#52c41a' }} />
|
||
|
|
<Badge count={record.invalidItems} style={{ backgroundColor: '#f5222d' }} />
|
||
|
|
</Space>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Amount',
|
||
|
|
dataIndex: 'totalAmount',
|
||
|
|
key: 'totalAmount',
|
||
|
|
width: 120,
|
||
|
|
render: (amount: number) => (
|
||
|
|
<span style={{ fontWeight: 'bold' }}>${amount.toFixed(2)}</span>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Created',
|
||
|
|
dataIndex: 'createdAt',
|
||
|
|
key: 'createdAt',
|
||
|
|
width: 160,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Actions',
|
||
|
|
key: 'action',
|
||
|
|
width: 150,
|
||
|
|
fixed: 'right',
|
||
|
|
render: (_, record) => (
|
||
|
|
<Space>
|
||
|
|
<Tooltip title="View Details">
|
||
|
|
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
|
||
|
|
</Tooltip>
|
||
|
|
{record.status === 'PENDING_REVIEW' && (
|
||
|
|
<Tooltip title="Approve">
|
||
|
|
<Button type="link" icon={<CheckCircleOutlined />} onClick={() => handleApproveBatch(record.batchId)} />
|
||
|
|
</Tooltip>
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const validCount = orderItems.filter((i) => i.status === 'VALID').length;
|
||
|
|
const invalidCount = orderItems.filter((i) => i.status === 'INVALID').length;
|
||
|
|
const totalAmount = orderItems.filter((i) => i.status === 'VALID').reduce((sum, item) => sum + item.totalPrice, 0);
|
||
|
|
|
||
|
|
const steps = [
|
||
|
|
{ title: 'Upload', description: 'Select customer & upload file' },
|
||
|
|
{ title: 'Review', description: 'Validate items' },
|
||
|
|
{ title: 'Submit', description: 'Confirm & submit' },
|
||
|
|
];
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="batch-order-page">
|
||
|
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||
|
|
<Col span={4}>
|
||
|
|
<Card>
|
||
|
|
<Statistic title="Total Batches" value={stats.total} prefix={<ShoppingCartOutlined />} />
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={4}>
|
||
|
|
<Card>
|
||
|
|
<Statistic
|
||
|
|
title="Pending Review"
|
||
|
|
value={stats.pending}
|
||
|
|
valueStyle={{ color: '#faad14' }}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={4}>
|
||
|
|
<Card>
|
||
|
|
<Statistic
|
||
|
|
title="Processing"
|
||
|
|
value={stats.processing}
|
||
|
|
valueStyle={{ color: '#1890ff' }}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={4}>
|
||
|
|
<Card>
|
||
|
|
<Statistic
|
||
|
|
title="Completed"
|
||
|
|
value={stats.completed}
|
||
|
|
valueStyle={{ color: '#52c41a' }}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Card>
|
||
|
|
<Statistic
|
||
|
|
title="Total Amount"
|
||
|
|
value={stats.totalAmount}
|
||
|
|
precision={2}
|
||
|
|
prefix="$"
|
||
|
|
valueStyle={{ color: '#52c41a' }}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Card
|
||
|
|
title="Batch Orders"
|
||
|
|
extra={
|
||
|
|
<Button type="primary" icon={<ShoppingCartOutlined />} onClick={() => {
|
||
|
|
setCreateModalVisible(true);
|
||
|
|
setCurrentStep(0);
|
||
|
|
setOrderItems([]);
|
||
|
|
setFileList([]);
|
||
|
|
}}>
|
||
|
|
Create Batch Order
|
||
|
|
</Button>
|
||
|
|
}
|
||
|
|
>
|
||
|
|
<Table
|
||
|
|
columns={batchColumns}
|
||
|
|
dataSource={batchOrders}
|
||
|
|
rowKey="id"
|
||
|
|
loading={loading}
|
||
|
|
scroll={{ x: 1100 }}
|
||
|
|
pagination={{
|
||
|
|
showSizeChanger: true,
|
||
|
|
showTotal: (total) => `Total ${total} batches`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Modal
|
||
|
|
title="Create Batch Order"
|
||
|
|
open={createModalVisible}
|
||
|
|
onCancel={() => setCreateModalVisible(false)}
|
||
|
|
footer={null}
|
||
|
|
width={1000}
|
||
|
|
>
|
||
|
|
<Steps current={currentStep} items={steps} style={{ marginBottom: 24 }} />
|
||
|
|
|
||
|
|
{currentStep === 0 && (
|
||
|
|
<Form form={form} layout="vertical">
|
||
|
|
<Form.Item name="customerId" label="Customer" rules={[{ required: true }]}>
|
||
|
|
<Select
|
||
|
|
placeholder="Select customer"
|
||
|
|
showSearch
|
||
|
|
optionFilterProp="children"
|
||
|
|
onChange={handleCustomerChange}
|
||
|
|
>
|
||
|
|
{customers.map((c) => (
|
||
|
|
<Option key={c.id} value={c.id}>
|
||
|
|
{c.name} ({c.company})
|
||
|
|
</Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
{selectedCustomer && (
|
||
|
|
<Alert
|
||
|
|
message={`Selected Customer: ${selectedCustomer.name} (${selectedCustomer.tier})`}
|
||
|
|
type="info"
|
||
|
|
showIcon
|
||
|
|
style={{ marginBottom: 16 }}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<Divider>Upload Order File</Divider>
|
||
|
|
|
||
|
|
<div style={{ marginBottom: 16 }}>
|
||
|
|
<Button icon={<DownloadOutlined />} onClick={handleDownloadTemplate}>
|
||
|
|
Download Template
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Upload
|
||
|
|
accept=".xlsx,.xls,.csv"
|
||
|
|
fileList={fileList}
|
||
|
|
onChange={handleFileUpload}
|
||
|
|
beforeUpload={() => false}
|
||
|
|
maxCount={1}
|
||
|
|
>
|
||
|
|
<Button icon={<UploadOutlined />} disabled={!selectedCustomer}>
|
||
|
|
Select File
|
||
|
|
</Button>
|
||
|
|
</Upload>
|
||
|
|
|
||
|
|
<div style={{ marginTop: 16, color: '#666' }}>
|
||
|
|
Supported formats: Excel (.xlsx, .xls), CSV (.csv)
|
||
|
|
</div>
|
||
|
|
</Form>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{currentStep === 1 && (
|
||
|
|
<>
|
||
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
|
|
<Col span={8}>
|
||
|
|
<Card size="small">
|
||
|
|
<Statistic
|
||
|
|
title="Valid Items"
|
||
|
|
value={validCount}
|
||
|
|
valueStyle={{ color: '#52c41a' }}
|
||
|
|
prefix={<CheckCircleOutlined />}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Card size="small">
|
||
|
|
<Statistic
|
||
|
|
title="Invalid Items"
|
||
|
|
value={invalidCount}
|
||
|
|
valueStyle={{ color: '#f5222d' }}
|
||
|
|
prefix={<CloseCircleOutlined />}
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Card size="small">
|
||
|
|
<Statistic
|
||
|
|
title="Total Amount"
|
||
|
|
value={totalAmount}
|
||
|
|
precision={2}
|
||
|
|
prefix="$"
|
||
|
|
/>
|
||
|
|
</Card>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<div style={{ marginBottom: 16 }}>
|
||
|
|
<Space>
|
||
|
|
<Button onClick={handleValidateItems} loading={loading}>
|
||
|
|
Re-validate
|
||
|
|
</Button>
|
||
|
|
<Button onClick={() => setCurrentStep(0)}>Back</Button>
|
||
|
|
</Space>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Table
|
||
|
|
columns={itemColumns}
|
||
|
|
dataSource={orderItems}
|
||
|
|
rowKey="key"
|
||
|
|
pagination={false}
|
||
|
|
scroll={{ y: 300 }}
|
||
|
|
size="small"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||
|
|
<Form.Item name="note" label="Note">
|
||
|
|
<TextArea rows={2} placeholder="Add notes (optional)" />
|
||
|
|
</Form.Item>
|
||
|
|
</Form>
|
||
|
|
|
||
|
|
<div style={{ textAlign: 'right', marginTop: 16 }}>
|
||
|
|
<Space>
|
||
|
|
<Button onClick={() => setCreateModalVisible(false)}>Cancel</Button>
|
||
|
|
<Button
|
||
|
|
type="primary"
|
||
|
|
onClick={handleSubmitBatch}
|
||
|
|
loading={loading}
|
||
|
|
disabled={validCount === 0}
|
||
|
|
>
|
||
|
|
Submit Batch Order
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{currentStep === 2 && (
|
||
|
|
<div style={{ textAlign: 'center', padding: '40px 0' }}>
|
||
|
|
<CheckCircleOutlined style={{ fontSize: 64, color: '#52c41a' }} />
|
||
|
|
<h2 style={{ marginTop: 24 }}>Batch Order Submitted</h2>
|
||
|
|
<p style={{ color: '#666' }}>
|
||
|
|
Your batch order has been submitted for review.
|
||
|
|
<br />
|
||
|
|
You will be notified once it is approved.
|
||
|
|
</p>
|
||
|
|
<Descriptions bordered column={1} style={{ maxWidth: 400, margin: '24px auto' }}>
|
||
|
|
<Descriptions.Item label="Batch ID">BO-2026-NEW</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Status">
|
||
|
|
<Tag color="processing">Pending Review</Tag>
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Valid Items">{validCount}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Total Amount">${totalAmount.toFixed(2)}</Descriptions.Item>
|
||
|
|
</Descriptions>
|
||
|
|
<Button type="primary" onClick={() => setCreateModalVisible(false)}>
|
||
|
|
Done
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
|
||
|
|
<Modal
|
||
|
|
title="Batch Order Details"
|
||
|
|
open={detailModalVisible}
|
||
|
|
onCancel={() => setDetailModalVisible(false)}
|
||
|
|
footer={[
|
||
|
|
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||
|
|
Close
|
||
|
|
</Button>,
|
||
|
|
]}
|
||
|
|
width={700}
|
||
|
|
>
|
||
|
|
{selectedBatch && (
|
||
|
|
<>
|
||
|
|
<Descriptions bordered column={2}>
|
||
|
|
<Descriptions.Item label="Batch ID">{selectedBatch.batchId}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Status">
|
||
|
|
<Tag color={BATCH_STATUS_MAP[selectedBatch.status].color}>
|
||
|
|
{BATCH_STATUS_MAP[selectedBatch.status].text}
|
||
|
|
</Tag>
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Customer">{selectedBatch.customerName}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Currency">{selectedBatch.currency}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Valid Items">{selectedBatch.validItems}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Invalid Items">{selectedBatch.invalidItems}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Total Amount" span={2}>
|
||
|
|
<span style={{ fontSize: 18, fontWeight: 'bold', color: '#1890ff' }}>
|
||
|
|
${selectedBatch.totalAmount.toFixed(2)}
|
||
|
|
</span>
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Created">{selectedBatch.createdAt}</Descriptions.Item>
|
||
|
|
</Descriptions>
|
||
|
|
|
||
|
|
<Divider>Validation Progress</Divider>
|
||
|
|
|
||
|
|
<Progress
|
||
|
|
percent={Math.round((selectedBatch.validItems / selectedBatch.totalItems) * 100)}
|
||
|
|
status={selectedBatch.invalidItems > 0 ? 'exception' : 'success'}
|
||
|
|
format={() => `${selectedBatch.validItems}/${selectedBatch.totalItems} valid`}
|
||
|
|
/>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default BatchOrder;
|