feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
This commit is contained in:
548
dashboard/src/pages/Orders/ExceptionOrder.tsx
Normal file
548
dashboard/src/pages/Orders/ExceptionOrder.tsx
Normal 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;
|
||||
322
dashboard/src/pages/Orders/OrderAggregation.tsx
Normal file
322
dashboard/src/pages/Orders/OrderAggregation.tsx
Normal 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;
|
||||
467
dashboard/src/pages/Orders/OrderDetail.tsx
Normal file
467
dashboard/src/pages/Orders/OrderDetail.tsx
Normal 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;
|
||||
544
dashboard/src/pages/Orders/OrderList.tsx
Normal file
544
dashboard/src/pages/Orders/OrderList.tsx
Normal 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;
|
||||
4
dashboard/src/pages/Orders/index.ts
Normal file
4
dashboard/src/pages/Orders/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { OrderList } from './OrderList';
|
||||
export { OrderDetail } from './OrderDetail';
|
||||
export { OrderAggregation } from './OrderAggregation';
|
||||
export { ExceptionOrder } from './ExceptionOrder';
|
||||
Reference in New Issue
Block a user