feat: 新增多模块功能与服务实现

新增广告计划、用户资产、B2B交易、合规规则等核心模型
实现爬虫工作器、贸易服务、现金流预测等业务服务
添加RBAC权限测试、压力测试等测试用例
完善扩展程序的消息处理与内容脚本功能
重构应用入口与文档生成器
更新项目规则与业务闭环分析文档
This commit is contained in:
2026-03-18 09:38:09 +08:00
parent 72cd7f6f45
commit 037e412aad
158 changed files with 50217 additions and 1313 deletions

View File

@@ -0,0 +1,548 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Tag,
Button,
Space,
Modal,
Form,
Input,
Select,
message,
Tooltip,
Row,
Col,
Statistic,
Badge,
Tabs,
Descriptions,
Divider,
Alert,
Popconfirm,
} from 'antd';
import {
WarningOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
EyeOutlined,
ReloadOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Option } = Select;
const { TabPane } = Tabs;
const { TextArea } = Input;
interface ExceptionOrder {
id: string;
orderId: string;
platform: string;
customerName: string;
totalAmount: number;
exceptionType: 'PAYMENT_FAILED' | 'OUT_OF_STOCK' | 'ADDRESS_INVALID' | 'FRAUD_SUSPECTED' | 'SHIPPING_FAILED' | 'OTHER';
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
status: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'ESCALATED';
description: string;
createdAt: string;
assignedTo?: string;
resolution?: string;
}
const EXCEPTION_TYPE_MAP: Record<string, { color: string; text: string }> = {
PAYMENT_FAILED: { color: 'red', text: 'Payment Failed' },
OUT_OF_STOCK: { color: 'orange', text: 'Out of Stock' },
ADDRESS_INVALID: { color: 'blue', text: 'Invalid Address' },
FRAUD_SUSPECTED: { color: 'purple', text: 'Fraud Suspected' },
SHIPPING_FAILED: { color: 'cyan', text: 'Shipping Failed' },
OTHER: { color: 'default', text: 'Other' },
};
const SEVERITY_MAP: Record<string, { color: string; text: string }> = {
LOW: { color: 'default', text: 'Low' },
MEDIUM: { color: 'blue', text: 'Medium' },
HIGH: { color: 'orange', text: 'High' },
CRITICAL: { color: 'red', text: 'Critical' },
};
const EXCEPTION_STATUS_MAP: Record<string, { color: string; text: string }> = {
OPEN: { color: 'error', text: 'Open' },
IN_PROGRESS: { color: 'processing', text: 'In Progress' },
RESOLVED: { color: 'success', text: 'Resolved' },
ESCALATED: { color: 'warning', text: 'Escalated' },
};
export const ExceptionOrder: React.FC = () => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [exceptions, setExceptions] = useState<ExceptionOrder[]>([]);
const [selectedException, setSelectedException] = useState<ExceptionOrder | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [resolveModalVisible, setResolveModalVisible] = useState(false);
const [activeTab, setActiveTab] = useState('all');
const [stats, setStats] = useState({
total: 0,
open: 0,
critical: 0,
resolved: 0,
});
useEffect(() => {
fetchExceptions();
}, []);
const fetchExceptions = async () => {
setLoading(true);
try {
const mockExceptions: ExceptionOrder[] = [
{
id: '1',
orderId: 'ORD-2026-001',
platform: 'AMAZON',
customerName: 'John Smith',
totalAmount: 159.99,
exceptionType: 'PAYMENT_FAILED',
severity: 'HIGH',
status: 'OPEN',
description: 'Payment authorization failed. Customer card was declined.',
createdAt: '2026-03-18 10:30:00',
},
{
id: '2',
orderId: 'ORD-2026-002',
platform: 'EBAY',
customerName: 'Emma Wilson',
totalAmount: 89.50,
exceptionType: 'OUT_OF_STOCK',
severity: 'MEDIUM',
status: 'IN_PROGRESS',
description: 'Item SKU-001 is out of stock. Need to source from supplier.',
createdAt: '2026-03-18 09:15:00',
assignedTo: 'Agent_001',
},
{
id: '3',
orderId: 'ORD-2026-003',
platform: 'SHOPIFY',
customerName: 'Michael Brown',
totalAmount: 245.00,
exceptionType: 'ADDRESS_INVALID',
severity: 'LOW',
status: 'RESOLVED',
description: 'Shipping address incomplete. Missing zip code.',
createdAt: '2026-03-17 16:45:00',
resolution: 'Contacted customer and obtained correct address.',
},
{
id: '4',
orderId: 'ORD-2026-004',
platform: 'SHOPEE',
customerName: 'Sarah Davis',
totalAmount: 780.00,
exceptionType: 'FRAUD_SUSPECTED',
severity: 'CRITICAL',
status: 'ESCALATED',
description: 'Multiple high-value orders from same IP address. Possible fraud.',
createdAt: '2026-03-18 08:00:00',
assignedTo: 'Fraud_Team',
},
{
id: '5',
orderId: 'ORD-2026-005',
platform: 'AMAZON',
customerName: 'David Lee',
totalAmount: 120.00,
exceptionType: 'SHIPPING_FAILED',
severity: 'HIGH',
status: 'OPEN',
description: 'Carrier returned package. Address undeliverable.',
createdAt: '2026-03-17 14:20:00',
},
];
setExceptions(mockExceptions);
calculateStats(mockExceptions);
} catch (error) {
message.error('Failed to load exception orders');
} finally {
setLoading(false);
}
};
const calculateStats = (exceptionList: ExceptionOrder[]) => {
setStats({
total: exceptionList.length,
open: exceptionList.filter((e) => e.status === 'OPEN').length,
critical: exceptionList.filter((e) => e.severity === 'CRITICAL').length,
resolved: exceptionList.filter((e) => e.status === 'RESOLVED').length,
});
};
const handleViewDetail = (exception: ExceptionOrder) => {
setSelectedException(exception);
setDetailModalVisible(true);
};
const handleResolve = (exception: ExceptionOrder) => {
setSelectedException(exception);
form.resetFields();
setResolveModalVisible(true);
};
const handleSubmitResolution = async () => {
try {
const values = await form.validateFields();
setLoading(true);
console.log('Resolving exception:', {
exceptionId: selectedException?.id,
resolution: values.resolution,
action: values.action,
});
await new Promise((resolve) => setTimeout(resolve, 1000));
message.success('Exception resolved successfully');
setResolveModalVisible(false);
fetchExceptions();
} catch (error) {
message.error('Failed to resolve exception');
} finally {
setLoading(false);
}
};
const handleEscalate = async (exceptionId: string) => {
setLoading(true);
try {
console.log('Escalating exception:', exceptionId);
await new Promise((resolve) => setTimeout(resolve, 500));
message.success('Exception escalated to supervisor');
fetchExceptions();
} catch (error) {
message.error('Failed to escalate exception');
} finally {
setLoading(false);
}
};
const handleAutoResolve = async (exceptionId: string) => {
setLoading(true);
try {
console.log('Auto-resolving exception:', exceptionId);
await new Promise((resolve) => setTimeout(resolve, 500));
message.success('Auto-resolution applied');
fetchExceptions();
} catch (error) {
message.error('Failed to auto-resolve');
} finally {
setLoading(false);
}
};
const getFilteredExceptions = () => {
if (activeTab === 'all') return exceptions;
if (activeTab === 'open') return exceptions.filter((e) => e.status === 'OPEN');
if (activeTab === 'in_progress') return exceptions.filter((e) => e.status === 'IN_PROGRESS');
if (activeTab === 'escalated') return exceptions.filter((e) => e.severity === 'CRITICAL' || e.status === 'ESCALATED');
if (activeTab === 'resolved') return exceptions.filter((e) => e.status === 'RESOLVED');
return exceptions;
};
const columns: ColumnsType<ExceptionOrder> = [
{
title: 'Order ID',
dataIndex: 'orderId',
key: 'orderId',
width: 130,
render: (text: string, record) => (
<a onClick={() => handleViewDetail(record)}>{text}</a>
),
},
{
title: 'Type',
dataIndex: 'exceptionType',
key: 'exceptionType',
width: 140,
render: (type: string) => {
const config = EXCEPTION_TYPE_MAP[type];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Severity',
dataIndex: 'severity',
key: 'severity',
width: 90,
render: (severity: string) => {
const config = SEVERITY_MAP[severity];
return (
<Tag
color={config.color}
icon={severity === 'CRITICAL' ? <WarningOutlined /> : undefined}
>
{config.text}
</Tag>
);
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 110,
render: (status: string) => {
const config = EXCEPTION_STATUS_MAP[status];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Customer',
dataIndex: 'customerName',
key: 'customerName',
width: 120,
},
{
title: 'Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
width: 90,
render: (amount: number) => <strong>${amount.toFixed(2)}</strong>,
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
ellipsis: true,
width: 200,
},
{
title: 'Created',
dataIndex: 'createdAt',
key: 'createdAt',
width: 150,
},
{
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 !== 'RESOLVED' && (
<>
<Tooltip title="Resolve">
<Button type="link" icon={<CheckCircleOutlined />} onClick={() => handleResolve(record)} />
</Tooltip>
<Popconfirm
title="Escalate this exception?"
onConfirm={() => handleEscalate(record.id)}
>
<Tooltip title="Escalate">
<Button type="link" icon={<ExclamationCircleOutlined />} />
</Tooltip>
</Popconfirm>
</>
)}
</Space>
),
},
];
return (
<div className="exception-order-page">
{stats.critical > 0 && (
<Alert
message={`${stats.critical} Critical Exception${stats.critical > 1 ? 's' : ''} Require Immediate Attention`}
type="error"
showIcon
icon={<WarningOutlined />}
style={{ marginBottom: 16 }}
/>
)}
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="Total Exceptions"
value={stats.total}
prefix={<WarningOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Open"
value={stats.open}
valueStyle={{ color: '#f5222d' }}
prefix={<ExclamationCircleOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Critical"
value={stats.critical}
valueStyle={{ color: '#faad14' }}
prefix={<WarningOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="Resolved"
value={stats.resolved}
valueStyle={{ color: '#52c41a' }}
prefix={<CheckCircleOutlined />}
/>
</Card>
</Col>
</Row>
<Card
title="Exception Order Management"
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchExceptions}>
Refresh
</Button>
</Space>
}
>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane
tab={<Badge count={stats.open} offset={[10, 0]} size="small">All</Badge>}
key="all"
/>
<TabPane tab="Open" key="open" />
<TabPane tab="In Progress" key="in_progress" />
<TabPane tab="Escalated" key="escalated" />
<TabPane tab="Resolved" key="resolved" />
</Tabs>
<Table
columns={columns}
dataSource={getFilteredExceptions()}
rowKey="id"
loading={loading}
scroll={{ x: 1300 }}
pagination={{
showSizeChanger: true,
showTotal: (total) => `Total ${total} exceptions`,
}}
/>
</Card>
<Modal
title="Exception Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
Close
</Button>,
selectedException?.status !== 'RESOLVED' && (
<Button
key="resolve"
type="primary"
onClick={() => {
setDetailModalVisible(false);
handleResolve(selectedException!);
}}
>
Resolve
</Button>
),
]}
width={700}
>
{selectedException && (
<>
<Descriptions bordered column={2}>
<Descriptions.Item label="Order ID">{selectedException.orderId}</Descriptions.Item>
<Descriptions.Item label="Platform">{selectedException.platform}</Descriptions.Item>
<Descriptions.Item label="Exception Type">
<Tag color={EXCEPTION_TYPE_MAP[selectedException.exceptionType].color}>
{EXCEPTION_TYPE_MAP[selectedException.exceptionType].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Severity">
<Tag color={SEVERITY_MAP[selectedException.severity].color}>
{SEVERITY_MAP[selectedException.severity].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Status">
<Tag color={EXCEPTION_STATUS_MAP[selectedException.status].color}>
{EXCEPTION_STATUS_MAP[selectedException.status].text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="Amount">${selectedException.totalAmount.toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="Customer">{selectedException.customerName}</Descriptions.Item>
<Descriptions.Item label="Assigned To">{selectedException.assignedTo || 'Unassigned'}</Descriptions.Item>
<Descriptions.Item label="Created">{selectedException.createdAt}</Descriptions.Item>
</Descriptions>
<Divider>Description</Divider>
<p>{selectedException.description}</p>
{selectedException.resolution && (
<>
<Divider>Resolution</Divider>
<p style={{ color: '#52c41a' }}>{selectedException.resolution}</p>
</>
)}
</>
)}
</Modal>
<Modal
title="Resolve Exception"
open={resolveModalVisible}
onCancel={() => setResolveModalVisible(false)}
onOk={handleSubmitResolution}
confirmLoading={loading}
width={600}
>
{selectedException && (
<>
<Alert
message={`${EXCEPTION_TYPE_MAP[selectedException.exceptionType].text} - ${selectedException.orderId}`}
description={selectedException.description}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form form={form} layout="vertical">
<Form.Item name="action" label="Action Taken" rules={[{ required: true }]}>
<Select placeholder="Select action">
<Option value="CONTACT_CUSTOMER">Contact Customer</Option>
<Option value="REFUND_ISSUED">Refund Issued</Option>
<Option value="REPLACEMENT_SENT">Replacement Sent</Option>
<Option value="ADDRESS_UPDATED">Address Updated</Option>
<Option value="STOCK_ALLOCATED">Stock Allocated</Option>
<Option value="ORDER_CANCELLED">Order Cancelled</Option>
<Option value="OTHER">Other</Option>
</Select>
</Form.Item>
<Form.Item name="resolution" label="Resolution Notes" rules={[{ required: true }]}>
<TextArea rows={4} placeholder="Describe how the exception was resolved..." />
</Form.Item>
</Form>
</>
)}
</Modal>
</div>
);
};
export default ExceptionOrder;

View File

@@ -0,0 +1,322 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Tag,
Button,
Space,
Modal,
Row,
Col,
Statistic,
Select,
DatePicker,
Tabs,
Badge,
message,
Segmented,
} from 'antd';
import {
SyncOutlined,
ReloadOutlined,
AppstoreOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TabPane } = Tabs;
interface AggregatedOrder {
id: string;
platformOrderId: string;
platform: 'AMAZON' | 'EBAY' | 'SHOPIFY' | 'SHOPEE' | 'LAZADA';
customerName: string;
totalAmount: number;
currency: string;
status: string;
createdAt: string;
shopId: string;
shopName: string;
}
const PLATFORM_MAP: Record<string, { color: string; text: string }> = {
AMAZON: { color: 'orange', text: 'Amazon' },
EBAY: { color: 'blue', text: 'eBay' },
SHOPIFY: { color: 'green', text: 'Shopify' },
SHOPEE: { color: 'red', text: 'Shopee' },
LAZADA: { color: 'purple', text: 'Lazada' },
};
export const OrderAggregation: React.FC = () => {
const [loading, setLoading] = useState(false);
const [orders, setOrders] = useState<AggregatedOrder[]>([]);
const [viewMode, setViewMode] = useState<string>('table');
const [selectedPlatform, setSelectedPlatform] = useState<string>('all');
const [stats, setStats] = useState({
total: 0,
amazon: 0,
ebay: 0,
shopify: 0,
shopee: 0,
lazada: 0,
});
useEffect(() => {
fetchAggregatedOrders();
}, [selectedPlatform]);
const fetchAggregatedOrders = async () => {
setLoading(true);
try {
const mockOrders: AggregatedOrder[] = [
{ id: '1', platformOrderId: 'AMZ-123456', platform: 'AMAZON', customerName: 'John Smith', totalAmount: 159.99, currency: 'USD', status: 'SHIPPED', createdAt: '2026-03-18 10:30:00', shopId: 'SHOP_001', shopName: 'US Store' },
{ id: '2', platformOrderId: 'EB-789012', platform: 'EBAY', customerName: 'Emma Wilson', totalAmount: 89.50, currency: 'USD', status: 'PENDING', createdAt: '2026-03-18 09:15:00', shopId: 'SHOP_002', shopName: 'eBay Outlet' },
{ id: '3', platformOrderId: 'SH-345678', platform: 'SHOPIFY', customerName: 'Michael Brown', totalAmount: 245.00, currency: 'USD', status: 'PROCESSING', createdAt: '2026-03-17 16:45:00', shopId: 'SHOP_003', shopName: 'Brand Store' },
{ id: '4', platformOrderId: 'SP-901234', platform: 'SHOPEE', customerName: 'Sarah Davis', totalAmount: 78.00, currency: 'USD', status: 'DELIVERED', createdAt: '2026-03-15 11:20:00', shopId: 'SHOP_004', shopName: 'Shopee Mall' },
{ id: '5', platformOrderId: 'LZ-567890', platform: 'LAZADA', customerName: 'David Lee', totalAmount: 120.00, currency: 'USD', status: 'SHIPPED', createdAt: '2026-03-18 08:00:00', shopId: 'SHOP_005', shopName: 'Lazada Seller' },
{ id: '6', platformOrderId: 'AMZ-234567', platform: 'AMAZON', customerName: 'Lisa Chen', totalAmount: 320.00, currency: 'USD', status: 'PENDING', createdAt: '2026-03-18 12:00:00', shopId: 'SHOP_001', shopName: 'US Store' },
{ id: '7', platformOrderId: 'EB-890123', platform: 'EBAY', customerName: 'Tom Wilson', totalAmount: 55.00, currency: 'USD', status: 'DELIVERED', createdAt: '2026-03-14 15:30:00', shopId: 'SHOP_002', shopName: 'eBay Outlet' },
{ id: '8', platformOrderId: 'SH-456789', platform: 'SHOPIFY', customerName: 'Amy Zhang', totalAmount: 199.99, currency: 'USD', status: 'SHIPPED', createdAt: '2026-03-16 14:00:00', shopId: 'SHOP_003', shopName: 'Brand Store' },
];
const filtered = selectedPlatform === 'all'
? mockOrders
: mockOrders.filter((o) => o.platform === selectedPlatform);
setOrders(filtered);
calculateStats(mockOrders);
} catch (error) {
message.error('Failed to load aggregated orders');
} finally {
setLoading(false);
}
};
const calculateStats = (orderList: AggregatedOrder[]) => {
setStats({
total: orderList.length,
amazon: orderList.filter((o) => o.platform === 'AMAZON').length,
ebay: orderList.filter((o) => o.platform === 'EBAY').length,
shopify: orderList.filter((o) => o.platform === 'SHOPIFY').length,
shopee: orderList.filter((o) => o.platform === 'SHOPEE').length,
lazada: orderList.filter((o) => o.platform === 'LAZADA').length,
});
};
const columns: ColumnsType<AggregatedOrder> = [
{
title: 'Platform Order ID',
dataIndex: 'platformOrderId',
key: 'platformOrderId',
width: 150,
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
render: (platform: string) => {
const config = PLATFORM_MAP[platform];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Shop',
dataIndex: 'shopName',
key: 'shopName',
width: 120,
},
{
title: 'Customer',
dataIndex: 'customerName',
key: 'customerName',
width: 140,
},
{
title: 'Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
width: 100,
render: (amount: number) => <strong>${amount.toFixed(2)}</strong>,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const colorMap: Record<string, string> = {
PENDING: 'warning',
PROCESSING: 'processing',
SHIPPED: 'blue',
DELIVERED: 'success',
};
return <Tag color={colorMap[status] || 'default'}>{status}</Tag>;
},
},
{
title: 'Created',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
},
];
return (
<div className="order-aggregation-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}>
<Card>
<Statistic
title="Total Orders"
value={stats.total}
prefix={<AppstoreOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Amazon"
value={stats.amazon}
valueStyle={{ color: '#fa8c16' }}
prefix={<Badge color="#fa8c16" />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="eBay"
value={stats.ebay}
valueStyle={{ color: '#1890ff' }}
prefix={<Badge color="#1890ff" />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Shopify"
value={stats.shopify}
valueStyle={{ color: '#52c41a' }}
prefix={<Badge color="#52c41a" />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Shopee"
value={stats.shopee}
valueStyle={{ color: '#f5222d' }}
prefix={<Badge color="#f5222d" />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Lazada"
value={stats.lazada}
valueStyle={{ color: '#722ed1' }}
prefix={<Badge color="#722ed1" />}
/>
</Card>
</Col>
</Row>
<Card
title="Multi-Platform Order Aggregation"
extra={
<Space>
<Select
value={selectedPlatform}
onChange={setSelectedPlatform}
style={{ width: 120 }}
>
<Option value="all">All Platforms</Option>
<Option value="AMAZON">Amazon</Option>
<Option value="EBAY">eBay</Option>
<Option value="SHOPIFY">Shopify</Option>
<Option value="SHOPEE">Shopee</Option>
<Option value="LAZADA">Lazada</Option>
</Select>
<Button icon={<ReloadOutlined />} onClick={fetchAggregatedOrders}>
Refresh
</Button>
</Space>
}
>
<Tabs defaultActiveKey="table">
<TabPane
tab={
<span>
<UnorderedListOutlined /> Table View
</span>
}
key="table"
>
<Table
columns={columns}
dataSource={orders}
rowKey="id"
loading={loading}
scroll={{ x: 900 }}
pagination={{
showSizeChanger: true,
showTotal: (total) => `Total ${total} orders`,
}}
/>
</TabPane>
<TabPane
tab={
<span>
<AppstoreOutlined /> Platform Summary
</span>
}
key="summary"
>
<Row gutter={[16, 16]}>
{Object.entries(PLATFORM_MAP).map(([key, config]) => {
const platformOrders = orders.filter((o) => o.platform === key);
const totalAmount = platformOrders.reduce((sum, o) => sum + o.totalAmount, 0);
return (
<Col span={8} key={key}>
<Card
title={<Tag color={config.color}>{config.text}</Tag>}
hoverable
style={{ cursor: 'pointer' }}
onClick={() => setSelectedPlatform(key)}
>
<Row>
<Col span={12}>
<Statistic
title="Orders"
value={platformOrders.length}
/>
</Col>
<Col span={12}>
<Statistic
title="Revenue"
value={totalAmount}
precision={2}
prefix="$"
/>
</Col>
</Row>
</Card>
</Col>
);
})}
</Row>
</TabPane>
</Tabs>
</Card>
</div>
);
};
export default OrderAggregation;

View File

@@ -0,0 +1,467 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Descriptions,
Tag,
Button,
Space,
Table,
Timeline,
Divider,
Row,
Col,
Statistic,
Steps,
message,
Tabs,
Tooltip,
Modal,
Form,
Input,
Select,
} from 'antd';
import {
ArrowLeftOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
SyncOutlined,
TruckOutlined,
ClockCircleOutlined,
DollarOutlined,
UserOutlined,
EnvironmentOutlined,
EditOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { TabPane } = Tabs;
const { TextArea } = Input;
interface OrderItem {
id: string;
sku: string;
productName: string;
quantity: number;
unitPrice: number;
totalPrice: number;
status: string;
}
interface ShippingInfo {
carrier: string;
trackingNumber: string;
estimatedDelivery: string;
shippedAt?: string;
}
interface OrderDetailData {
orderId: string;
platform: string;
status: string;
paymentStatus: string;
customer: {
name: string;
email: string;
phone: string;
};
shippingAddress: {
name: string;
address: string;
city: string;
state: string;
country: string;
postalCode: string;
};
items: OrderItem[];
subtotal: number;
shippingFee: number;
tax: number;
totalAmount: number;
currency: string;
shippingInfo?: ShippingInfo;
timeline: Array<{
status: string;
time: string;
description: string;
}>;
createdAt: string;
updatedAt: string;
}
const ORDER_STATUS_MAP: Record<string, { color: string; text: string }> = {
PULLED: { color: 'default', text: 'Pulled' },
PENDING_REVIEW: { color: 'processing', text: 'Pending Review' },
CONFIRMED: { color: 'blue', text: 'Confirmed' },
ALLOCATED: { color: 'cyan', text: 'Allocated' },
READY_TO_SHIP: { color: 'geekblue', text: 'Ready to Ship' },
SHIPPED: { color: 'purple', text: 'Shipped' },
DELIVERED: { color: 'success', text: 'Delivered' },
CANCELLED: { color: 'error', text: 'Cancelled' },
};
const STATUS_STEPS = [
{ title: 'Pulled', status: 'PULLED' },
{ title: 'Review', status: 'PENDING_REVIEW' },
{ title: 'Confirmed', status: 'CONFIRMED' },
{ title: 'Allocated', status: 'ALLOCATED' },
{ title: 'Ready', status: 'READY_TO_SHIP' },
{ title: 'Shipped', status: 'SHIPPED' },
{ title: 'Delivered', status: 'DELIVERED' },
];
interface OrderDetailProps {
orderId?: string;
onBack?: () => void;
}
export const OrderDetail: React.FC<OrderDetailProps> = ({ orderId, onBack }) => {
const [loading, setLoading] = useState(false);
const [orderData, setOrderData] = useState<OrderDetailData | null>(null);
const [editModalVisible, setEditModalVisible] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
if (orderId) {
fetchOrderDetail(orderId);
}
}, [orderId]);
const fetchOrderDetail = async (id: string) => {
setLoading(true);
try {
const mockData: OrderDetailData = {
orderId: id || 'ORD-2026-001',
platform: 'AMAZON',
status: 'SHIPPED',
paymentStatus: 'PAID',
customer: {
name: 'John Smith',
email: 'john.smith@email.com',
phone: '+1 (555) 123-4567',
},
shippingAddress: {
name: 'John Smith',
address: '123 Main Street, Apt 4B',
city: 'New York',
state: 'NY',
country: 'United States',
postalCode: '10001',
},
items: [
{ id: '1', sku: 'SKU-001', productName: 'Wireless Bluetooth Headphones', quantity: 2, unitPrice: 49.99, totalPrice: 99.98, status: 'SHIPPED' },
{ id: '2', sku: 'SKU-002', productName: 'USB-C Charging Cable', quantity: 3, unitPrice: 9.99, totalPrice: 29.97, status: 'SHIPPED' },
{ id: '3', sku: 'SKU-003', productName: 'Phone Case', quantity: 1, unitPrice: 19.99, totalPrice: 19.99, status: 'SHIPPED' },
],
subtotal: 149.94,
shippingFee: 5.99,
tax: 12.06,
totalAmount: 167.99,
currency: 'USD',
shippingInfo: {
carrier: 'FedEx',
trackingNumber: 'FX123456789US',
estimatedDelivery: '2026-03-22',
shippedAt: '2026-03-18 14:00:00',
},
timeline: [
{ status: 'PULLED', time: '2026-03-18 10:30:00', description: 'Order pulled from Amazon' },
{ status: 'PENDING_REVIEW', time: '2026-03-18 10:35:00', description: 'Order submitted for review' },
{ status: 'CONFIRMED', time: '2026-03-18 11:00:00', description: 'Order confirmed by operator' },
{ status: 'ALLOCATED', time: '2026-03-18 12:00:00', description: 'Stock allocated from warehouse' },
{ status: 'READY_TO_SHIP', time: '2026-03-18 13:00:00', description: 'Order packed and ready' },
{ status: 'SHIPPED', time: '2026-03-18 14:00:00', description: 'Shipped via FedEx' },
],
createdAt: '2026-03-18 10:30:00',
updatedAt: '2026-03-18 14:00:00',
};
setOrderData(mockData);
} catch (error) {
message.error('Failed to load order details');
} finally {
setLoading(false);
}
};
const handleStatusUpdate = async (newStatus: string) => {
setLoading(true);
try {
console.log('Updating status to:', newStatus);
await new Promise((resolve) => setTimeout(resolve, 500));
message.success('Status updated successfully');
if (orderId) fetchOrderDetail(orderId);
} catch (error) {
message.error('Failed to update status');
} finally {
setLoading(false);
}
};
const handleEditAddress = () => {
if (orderData) {
form.setFieldsValue(orderData.shippingAddress);
setEditModalVisible(true);
}
};
const handleSaveAddress = async () => {
try {
const values = await form.validateFields();
console.log('Saving address:', values);
await new Promise((resolve) => setTimeout(resolve, 500));
message.success('Address updated successfully');
setEditModalVisible(false);
} catch (error) {
message.error('Failed to save address');
}
};
const getCurrentStep = () => {
if (!orderData) return 0;
const index = STATUS_STEPS.findIndex((s) => s.status === orderData.status);
return index >= 0 ? index : 0;
};
const itemColumns: ColumnsType<OrderItem> = [
{ title: 'SKU', dataIndex: 'sku', key: 'sku', width: 100 },
{ title: 'Product', dataIndex: 'productName', key: 'productName' },
{ title: 'Qty', dataIndex: 'quantity', key: 'quantity', width: 60, align: 'center' },
{
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) => <strong>${price.toFixed(2)}</strong>,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => <Tag color="green">{status}</Tag>,
},
];
if (!orderData) {
return <Card loading={loading}>Loading...</Card>;
}
return (
<div className="order-detail-page">
<Card
title={
<Space>
{onBack && (
<Button icon={<ArrowLeftOutlined />} onClick={onBack}>
Back
</Button>
)}
<span>Order Details</span>
</Space>
}
extra={
<Space>
<Tag color="blue" style={{ fontSize: 14, padding: '4px 12px' }}>
{orderData.orderId}
</Tag>
<Tag color={ORDER_STATUS_MAP[orderData.status].color} style={{ fontSize: 14, padding: '4px 12px' }}>
{ORDER_STATUS_MAP[orderData.status].text}
</Tag>
</Space>
}
>
<Steps
current={getCurrentStep()}
status={orderData.status === 'CANCELLED' ? 'error' : 'process'}
items={STATUS_STEPS.map((s) => ({ title: s.title }))}
style={{ marginBottom: 24 }}
/>
<Tabs defaultActiveKey="overview">
<TabPane tab="Overview" key="overview">
<Row gutter={24}>
<Col span={16}>
<Card title="Order Items" size="small">
<Table
columns={itemColumns}
dataSource={orderData.items}
rowKey="id"
pagination={false}
size="small"
summary={() => (
<>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">Subtotal</Table.Summary.Cell>
<Table.Summary.Cell index={1} colSpan={2}>${orderData.subtotal.toFixed(2)}</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">Shipping</Table.Summary.Cell>
<Table.Summary.Cell index={1} colSpan={2}>${orderData.shippingFee.toFixed(2)}</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">Tax</Table.Summary.Cell>
<Table.Summary.Cell index={1} colSpan={2}>${orderData.tax.toFixed(2)}</Table.Summary.Cell>
</Table.Summary.Row>
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right"><strong>Total</strong></Table.Summary.Cell>
<Table.Summary.Cell index={1} colSpan={2}>
<strong style={{ fontSize: 16, color: '#1890ff' }}>
${orderData.totalAmount.toFixed(2)}
</strong>
</Table.Summary.Cell>
</Table.Summary.Row>
</>
)}
/>
</Card>
<Card title="Shipping Information" size="small" style={{ marginTop: 16 }} extra={
<Button type="link" icon={<EditOutlined />} onClick={handleEditAddress}>Edit</Button>
}>
<Descriptions column={2} size="small">
<Descriptions.Item label="Carrier">{orderData.shippingInfo?.carrier || '-'}</Descriptions.Item>
<Descriptions.Item label="Tracking #">{orderData.shippingInfo?.trackingNumber || '-'}</Descriptions.Item>
<Descriptions.Item label="Shipped At">{orderData.shippingInfo?.shippedAt || '-'}</Descriptions.Item>
<Descriptions.Item label="Est. Delivery">{orderData.shippingInfo?.estimatedDelivery || '-'}</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={8}>
<Card title="Customer" size="small">
<p><UserOutlined /> {orderData.customer.name}</p>
<p>{orderData.customer.email}</p>
<p>{orderData.customer.phone}</p>
</Card>
<Card title="Shipping Address" size="small" style={{ marginTop: 16 }}>
<p><EnvironmentOutlined /> {orderData.shippingAddress.name}</p>
<p>{orderData.shippingAddress.address}</p>
<p>{orderData.shippingAddress.city}, {orderData.shippingAddress.state} {orderData.shippingAddress.postalCode}</p>
<p>{orderData.shippingAddress.country}</p>
</Card>
<Card size="small" style={{ marginTop: 16 }}>
<Row gutter={16}>
<Col span={12}>
<Statistic title="Items" value={orderData.items.length} />
</Col>
<Col span={12}>
<Statistic title="Total Qty" value={orderData.items.reduce((sum, i) => sum + i.quantity, 0)} />
</Col>
</Row>
</Card>
</Col>
</Row>
</TabPane>
<TabPane tab="Timeline" key="timeline">
<Timeline
items={orderData.timeline.map((t) => ({
color: t.status === orderData.status ? 'green' : 'blue',
children: (
<div>
<p><strong>{t.description}</strong></p>
<p style={{ color: '#666', margin: 0 }}>{t.time}</p>
</div>
),
}))}
/>
</TabPane>
<TabPane tab="Actions" key="actions">
<Space direction="vertical" style={{ width: '100%' }}>
{orderData.status === 'PENDING_REVIEW' && (
<Card size="small" title="Review Actions">
<Space>
<Button type="primary" icon={<CheckCircleOutlined />} onClick={() => handleStatusUpdate('CONFIRMED')}>
Confirm Order
</Button>
<Button danger icon={<CloseCircleOutlined />} onClick={() => handleStatusUpdate('CANCELLED')}>
Cancel Order
</Button>
</Space>
</Card>
)}
{orderData.status === 'CONFIRMED' && (
<Card size="small" title="Fulfillment Actions">
<Space>
<Button type="primary" onClick={() => handleStatusUpdate('ALLOCATED')}>
Allocate Stock
</Button>
</Space>
</Card>
)}
{orderData.status === 'ALLOCATED' && (
<Card size="small" title="Shipping Actions">
<Space>
<Button type="primary" icon={<TruckOutlined />} onClick={() => handleStatusUpdate('READY_TO_SHIP')}>
Mark Ready to Ship
</Button>
</Space>
</Card>
)}
{orderData.status === 'READY_TO_SHIP' && (
<Card size="small" title="Shipping Actions">
<Space>
<Button type="primary" icon={<TruckOutlined />} onClick={() => handleStatusUpdate('SHIPPED')}>
Mark as Shipped
</Button>
</Space>
</Card>
)}
</Space>
</TabPane>
</Tabs>
</Card>
<Modal
title="Edit Shipping Address"
open={editModalVisible}
onCancel={() => setEditModalVisible(false)}
onOk={handleSaveAddress}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="Name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item name="address" label="Address" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="city" label="City" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="state" label="State" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="postalCode" label="Postal Code" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="country" label="Country" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</div>
);
};
export default OrderDetail;

View File

@@ -0,0 +1,544 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Tag,
Button,
Space,
Modal,
Form,
Input,
Select,
DatePicker,
message,
Tooltip,
Row,
Col,
Statistic,
Badge,
Dropdown,
Menu,
Tabs,
} from 'antd';
import {
EyeOutlined,
SyncOutlined,
DownloadOutlined,
ReloadOutlined,
FilterOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
TruckOutlined,
CloseCircleOutlined,
MoreOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TabPane } = Tabs;
interface Order {
id: string;
orderId: string;
platform: 'AMAZON' | 'EBAY' | 'SHOPIFY' | 'SHOPEE' | 'LAZADA';
customerName: string;
customerEmail: string;
totalAmount: number;
currency: string;
status: 'PULLED' | 'PENDING_REVIEW' | 'CONFIRMED' | 'ALLOCATED' | 'READY_TO_SHIP' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
paymentStatus: 'PENDING' | 'PAID' | 'REFUNDED';
itemCount: number;
createdAt: string;
updatedAt: string;
shopId: string;
shopName: string;
}
const ORDER_STATUS_MAP: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
PULLED: { color: 'default', text: 'Pulled', icon: <ClockCircleOutlined /> },
PENDING_REVIEW: { color: 'processing', text: 'Pending Review', icon: <SyncOutlined spin /> },
CONFIRMED: { color: 'blue', text: 'Confirmed', icon: <CheckCircleOutlined /> },
ALLOCATED: { color: 'cyan', text: 'Allocated', icon: <CheckCircleOutlined /> },
READY_TO_SHIP: { color: 'geekblue', text: 'Ready to Ship', icon: <TruckOutlined /> },
SHIPPED: { color: 'purple', text: 'Shipped', icon: <TruckOutlined /> },
DELIVERED: { color: 'success', text: 'Delivered', icon: <CheckCircleOutlined /> },
CANCELLED: { color: 'error', text: 'Cancelled', icon: <CloseCircleOutlined /> },
};
const PAYMENT_STATUS_MAP: Record<string, { color: string; text: string }> = {
PENDING: { color: 'warning', text: 'Pending' },
PAID: { color: 'success', text: 'Paid' },
REFUNDED: { color: 'default', text: 'Refunded' },
};
const PLATFORM_MAP: Record<string, { color: string; text: string }> = {
AMAZON: { color: 'orange', text: 'Amazon' },
EBAY: { color: 'blue', text: 'eBay' },
SHOPIFY: { color: 'green', text: 'Shopify' },
SHOPEE: { color: 'red', text: 'Shopee' },
LAZADA: { color: 'purple', text: 'Lazada' },
};
export const OrderList: React.FC = () => {
const [loading, setLoading] = useState(false);
const [orders, setOrders] = useState<Order[]>([]);
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
const [detailModalVisible, setDetailModalVisible] = useState(false);
const [activeTab, setActiveTab] = useState('all');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [stats, setStats] = useState({
total: 0,
pending: 0,
processing: 0,
shipped: 0,
totalAmount: 0,
});
useEffect(() => {
fetchOrders();
}, []);
const fetchOrders = async () => {
setLoading(true);
try {
const mockOrders: Order[] = [
{
id: '1',
orderId: 'ORD-2026-001',
platform: 'AMAZON',
customerName: 'John Smith',
customerEmail: 'john.smith@email.com',
totalAmount: 159.99,
currency: 'USD',
status: 'SHIPPED',
paymentStatus: 'PAID',
itemCount: 3,
createdAt: '2026-03-18 10:30:00',
updatedAt: '2026-03-18 14:00:00',
shopId: 'SHOP_001',
shopName: 'Main Store',
},
{
id: '2',
orderId: 'ORD-2026-002',
platform: 'EBAY',
customerName: 'Emma Wilson',
customerEmail: 'emma.wilson@email.com',
totalAmount: 89.50,
currency: 'USD',
status: 'PENDING_REVIEW',
paymentStatus: 'PAID',
itemCount: 2,
createdAt: '2026-03-18 09:15:00',
updatedAt: '2026-03-18 09:15:00',
shopId: 'SHOP_002',
shopName: 'eBay Outlet',
},
{
id: '3',
orderId: 'ORD-2026-003',
platform: 'SHOPIFY',
customerName: 'Michael Brown',
customerEmail: 'michael.brown@email.com',
totalAmount: 245.00,
currency: 'USD',
status: 'CONFIRMED',
paymentStatus: 'PAID',
itemCount: 5,
createdAt: '2026-03-17 16:45:00',
updatedAt: '2026-03-18 08:00:00',
shopId: 'SHOP_003',
shopName: 'Brand Store',
},
{
id: '4',
orderId: 'ORD-2026-004',
platform: 'SHOPEE',
customerName: 'Sarah Davis',
customerEmail: 'sarah.davis@email.com',
totalAmount: 78.00,
currency: 'USD',
status: 'DELIVERED',
paymentStatus: 'PAID',
itemCount: 1,
createdAt: '2026-03-15 11:20:00',
updatedAt: '2026-03-17 15:30:00',
shopId: 'SHOP_004',
shopName: 'Shopee Mall',
},
{
id: '5',
orderId: 'ORD-2026-005',
platform: 'AMAZON',
customerName: 'David Lee',
customerEmail: 'david.lee@email.com',
totalAmount: 320.00,
currency: 'USD',
status: 'PULLED',
paymentStatus: 'PENDING',
itemCount: 4,
createdAt: '2026-03-18 12:00:00',
updatedAt: '2026-03-18 12:00:00',
shopId: 'SHOP_001',
shopName: 'Main Store',
},
];
setOrders(mockOrders);
calculateStats(mockOrders);
} catch (error) {
message.error('Failed to load orders');
} finally {
setLoading(false);
}
};
const calculateStats = (orderList: Order[]) => {
setStats({
total: orderList.length,
pending: orderList.filter((o) => o.status === 'PENDING_REVIEW' || o.status === 'PULLED').length,
processing: orderList.filter((o) => ['CONFIRMED', 'ALLOCATED', 'READY_TO_SHIP'].includes(o.status)).length,
shipped: orderList.filter((o) => o.status === 'SHIPPED').length,
totalAmount: orderList.reduce((sum, o) => sum + o.totalAmount, 0),
});
};
const handleViewDetail = (order: Order) => {
setSelectedOrder(order);
setDetailModalVisible(true);
};
const handleStatusChange = async (orderId: string, newStatus: string) => {
setLoading(true);
try {
console.log('Updating order status:', { orderId, newStatus });
await new Promise((resolve) => setTimeout(resolve, 500));
message.success('Status updated successfully');
fetchOrders();
} catch (error) {
message.error('Failed to update status');
} finally {
setLoading(false);
}
};
const handleBatchAction = async (action: string) => {
if (selectedRowKeys.length === 0) {
message.warning('Please select orders first');
return;
}
setLoading(true);
try {
console.log('Batch action:', { action, orderIds: selectedRowKeys });
await new Promise((resolve) => setTimeout(resolve, 500));
message.success(`Batch ${action} completed for ${selectedRowKeys.length} orders`);
setSelectedRowKeys([]);
fetchOrders();
} catch (error) {
message.error('Batch action failed');
} finally {
setLoading(false);
}
};
const handleExport = () => {
message.info('Exporting orders...');
};
const getFilteredOrders = () => {
if (activeTab === 'all') return orders;
if (activeTab === 'pending') return orders.filter((o) => ['PULLED', 'PENDING_REVIEW'].includes(o.status));
if (activeTab === 'processing') return orders.filter((o) => ['CONFIRMED', 'ALLOCATED', 'READY_TO_SHIP'].includes(o.status));
if (activeTab === 'shipped') return orders.filter((o) => o.status === 'SHIPPED');
if (activeTab === 'delivered') return orders.filter((o) => o.status === 'DELIVERED');
return orders;
};
const columns: ColumnsType<Order> = [
{
title: 'Order ID',
dataIndex: 'orderId',
key: 'orderId',
width: 140,
render: (text: string, record) => (
<a onClick={() => handleViewDetail(record)}>{text}</a>
),
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
width: 100,
render: (platform: string) => {
const config = PLATFORM_MAP[platform];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Customer',
dataIndex: 'customerName',
key: 'customerName',
width: 140,
},
{
title: 'Items',
dataIndex: 'itemCount',
key: 'itemCount',
width: 70,
align: 'center',
},
{
title: 'Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
width: 100,
render: (amount: number) => (
<span style={{ fontWeight: 'bold' }}>${amount.toFixed(2)}</span>
),
},
{
title: 'Payment',
dataIndex: 'paymentStatus',
key: 'paymentStatus',
width: 90,
render: (status: string) => {
const config = PAYMENT_STATUS_MAP[status];
return <Tag color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
width: 130,
render: (status: string) => {
const config = ORDER_STATUS_MAP[status];
return <Tag icon={config.icon} color={config.color}>{config.text}</Tag>;
},
},
{
title: 'Shop',
dataIndex: 'shopName',
key: 'shopName',
width: 120,
},
{
title: 'Created',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
},
{
title: 'Actions',
key: 'action',
width: 120,
fixed: 'right',
render: (_, record) => (
<Space>
<Tooltip title="View Details">
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
</Tooltip>
<Dropdown
overlay={
<Menu>
{record.status === 'PULLED' && (
<Menu.Item key="review" onClick={() => handleStatusChange(record.orderId, 'PENDING_REVIEW')}>
Submit for Review
</Menu.Item>
)}
{record.status === 'PENDING_REVIEW' && (
<Menu.Item key="confirm" onClick={() => handleStatusChange(record.orderId, 'CONFIRMED')}>
Confirm Order
</Menu.Item>
)}
{record.status === 'CONFIRMED' && (
<Menu.Item key="allocate" onClick={() => handleStatusChange(record.orderId, 'ALLOCATED')}>
Allocate Stock
</Menu.Item>
)}
<Menu.Item key="cancel" onClick={() => handleStatusChange(record.orderId, 'CANCELLED')}>
Cancel Order
</Menu.Item>
</Menu>
}
>
<Button type="link" icon={<MoreOutlined />} />
</Dropdown>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: setSelectedRowKeys,
};
return (
<div className="order-list-page">
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}>
<Card>
<Statistic title="Total Orders" value={stats.total} />
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Pending"
value={stats.pending}
valueStyle={{ color: '#faad14' }}
prefix={<ClockCircleOutlined />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Processing"
value={stats.processing}
valueStyle={{ color: '#1890ff' }}
prefix={<SyncOutlined spin />}
/>
</Card>
</Col>
<Col span={4}>
<Card>
<Statistic
title="Shipped"
value={stats.shipped}
valueStyle={{ color: '#722ed1' }}
prefix={<TruckOutlined />}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="Total Amount"
value={stats.totalAmount}
precision={2}
prefix="$"
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
</Row>
<Card
title="Order Management"
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchOrders}>
Refresh
</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>
Export
</Button>
</Space>
}
>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="All Orders" key="all" />
<TabPane tab={<Badge count={stats.pending} offset={[10, 0]} size="small">Pending</Badge>} key="pending" />
<TabPane tab="Processing" key="processing" />
<TabPane tab="Shipped" key="shipped" />
<TabPane tab="Delivered" key="delivered" />
</Tabs>
<div style={{ marginBottom: 16 }}>
<Space>
<Button
type="primary"
disabled={selectedRowKeys.length === 0}
onClick={() => handleBatchAction('confirm')}
>
Batch Confirm ({selectedRowKeys.length})
</Button>
<Button
disabled={selectedRowKeys.length === 0}
onClick={() => handleBatchAction('allocate')}
>
Batch Allocate
</Button>
<Button
danger
disabled={selectedRowKeys.length === 0}
onClick={() => handleBatchAction('cancel')}
>
Batch Cancel
</Button>
</Space>
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={getFilteredOrders()}
rowKey="id"
loading={loading}
scroll={{ x: 1300 }}
pagination={{
showSizeChanger: true,
showTotal: (total) => `Total ${total} orders`,
}}
/>
</Card>
<Modal
title="Order Details"
open={detailModalVisible}
onCancel={() => setDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setDetailModalVisible(false)}>
Close
</Button>,
]}
width={700}
>
{selectedOrder && (
<>
<Row gutter={16}>
<Col span={12}>
<Card size="small" title="Order Info">
<p><strong>Order ID:</strong> {selectedOrder.orderId}</p>
<p><strong>Platform:</strong> <Tag color={PLATFORM_MAP[selectedOrder.platform].color}>{PLATFORM_MAP[selectedOrder.platform].text}</Tag></p>
<p><strong>Status:</strong> <Tag color={ORDER_STATUS_MAP[selectedOrder.status].color}>{ORDER_STATUS_MAP[selectedOrder.status].text}</Tag></p>
<p><strong>Payment:</strong> <Tag color={PAYMENT_STATUS_MAP[selectedOrder.paymentStatus].color}>{PAYMENT_STATUS_MAP[selectedOrder.paymentStatus].text}</Tag></p>
</Card>
</Col>
<Col span={12}>
<Card size="small" title="Customer Info">
<p><strong>Name:</strong> {selectedOrder.customerName}</p>
<p><strong>Email:</strong> {selectedOrder.customerEmail}</p>
<p><strong>Shop:</strong> {selectedOrder.shopName}</p>
</Card>
</Col>
</Row>
<Card size="small" title="Amount" style={{ marginTop: 16 }}>
<Row>
<Col span={12}>
<Statistic
title="Total Amount"
value={selectedOrder.totalAmount}
precision={2}
prefix={selectedOrder.currency === 'USD' ? '$' : selectedOrder.currency}
/>
</Col>
<Col span={12}>
<Statistic title="Items" value={selectedOrder.itemCount} />
</Col>
</Row>
</Card>
<Card size="small" title="Timestamps" style={{ marginTop: 16 }}>
<p><strong>Created:</strong> {selectedOrder.createdAt}</p>
<p><strong>Updated:</strong> {selectedOrder.updatedAt}</p>
</Card>
</>
)}
</Modal>
</div>
);
};
export default OrderList;

View File

@@ -0,0 +1,4 @@
export { OrderList } from './OrderList';
export { OrderDetail } from './OrderDetail';
export { OrderAggregation } from './OrderAggregation';
export { ExceptionOrder } from './ExceptionOrder';