feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
This commit is contained in:
394
dashboard/src/pages/Compliance/CertificateExpiryReminder.tsx
Normal file
394
dashboard/src/pages/Compliance/CertificateExpiryReminder.tsx
Normal file
@@ -0,0 +1,394 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Tag,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Alert,
|
||||
List,
|
||||
Badge,
|
||||
Tooltip,
|
||||
Switch,
|
||||
message,
|
||||
Divider,
|
||||
Timeline,
|
||||
} from 'antd';
|
||||
import {
|
||||
BellOutlined,
|
||||
ClockCircleOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
SettingOutlined,
|
||||
MailOutlined,
|
||||
SyncOutlined,
|
||||
FileTextOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
interface ExpiryReminder {
|
||||
id: string;
|
||||
certNo: string;
|
||||
certName: string;
|
||||
certType: string;
|
||||
productName: string;
|
||||
expiryDate: string;
|
||||
daysRemaining: number;
|
||||
status: 'CRITICAL' | 'WARNING' | 'NOTICE' | 'SAFE';
|
||||
reminderSent: boolean;
|
||||
lastReminderDate?: string;
|
||||
nextReminderDate?: string;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
const MOCK_REMINDERS: ExpiryReminder[] = [
|
||||
{
|
||||
id: '1',
|
||||
certNo: 'FCC-2025-045',
|
||||
certName: 'FCC Declaration',
|
||||
certType: 'FCC',
|
||||
productName: 'Control Module B',
|
||||
expiryDate: '2025-06-19',
|
||||
daysRemaining: 93,
|
||||
status: 'WARNING',
|
||||
reminderSent: true,
|
||||
lastReminderDate: '2025-03-15',
|
||||
nextReminderDate: '2025-04-15',
|
||||
actions: ['Initiate renewal process', 'Contact testing lab'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
certNo: 'RoHS-2024-112',
|
||||
certName: 'RoHS Compliance Certificate',
|
||||
certType: 'RoHS',
|
||||
productName: 'Power Supply Unit C',
|
||||
expiryDate: '2025-02-28',
|
||||
daysRemaining: -18,
|
||||
status: 'CRITICAL',
|
||||
reminderSent: true,
|
||||
lastReminderDate: '2025-03-01',
|
||||
actions: ['Immediate re-certification required', 'Suspend product listing'],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
certNo: 'CE-2026-001',
|
||||
certName: 'CE Declaration of Conformity',
|
||||
certType: 'CE',
|
||||
productName: 'Industrial Sensor A',
|
||||
expiryDate: '2027-01-14',
|
||||
daysRemaining: 663,
|
||||
status: 'SAFE',
|
||||
reminderSent: false,
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
certNo: 'UL-2026-008',
|
||||
certName: 'UL Safety Certification',
|
||||
certType: 'UL',
|
||||
productName: 'Communication Gateway D',
|
||||
expiryDate: '2027-01-31',
|
||||
daysRemaining: 680,
|
||||
status: 'SAFE',
|
||||
reminderSent: false,
|
||||
actions: [],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
certNo: 'ISO-2025-022',
|
||||
certName: 'ISO 9001:2015',
|
||||
certType: 'ISO',
|
||||
productName: 'Company-wide',
|
||||
expiryDate: '2025-09-30',
|
||||
daysRemaining: 196,
|
||||
status: 'NOTICE',
|
||||
reminderSent: true,
|
||||
lastReminderDate: '2025-03-01',
|
||||
nextReminderDate: '2025-06-01',
|
||||
actions: ['Schedule surveillance audit', 'Update quality manual'],
|
||||
},
|
||||
];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode; bgColor: string }> = {
|
||||
CRITICAL: { color: 'error', text: 'Critical', icon: <CloseCircleOutlined />, bgColor: '#fff2f0' },
|
||||
WARNING: { color: 'warning', text: 'Warning', icon: <WarningOutlined />, bgColor: '#fffbe6' },
|
||||
NOTICE: { color: 'processing', text: 'Notice', icon: <ClockCircleOutlined />, bgColor: '#e6f7ff' },
|
||||
SAFE: { color: 'success', text: 'Safe', icon: <CheckCircleOutlined />, bgColor: '#f6ffed' },
|
||||
};
|
||||
|
||||
export const CertificateExpiryReminder: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [reminders, setReminders] = useState<ExpiryReminder[]>(MOCK_REMINDERS);
|
||||
const [settingsModalVisible, setSettingsModalVisible] = useState(false);
|
||||
const [emailEnabled, setEmailEnabled] = useState(true);
|
||||
const [autoRenewal, setAutoRenewal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkReminders();
|
||||
}, []);
|
||||
|
||||
const checkReminders = () => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
setLoading(false);
|
||||
message.success('Reminders checked');
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleSendReminder = (reminder: ExpiryReminder) => {
|
||||
Modal.confirm({
|
||||
title: 'Send Reminder',
|
||||
content: `Send expiry reminder for ${reminder.certNo}?`,
|
||||
onOk: () => {
|
||||
setReminders(reminders.map(r =>
|
||||
r.id === reminder.id ? { ...r, reminderSent: true, lastReminderDate: new Date().toISOString().split('T')[0] } : r
|
||||
));
|
||||
message.success('Reminder sent successfully');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleInitiateRenewal = (reminder: ExpiryReminder) => {
|
||||
Modal.confirm({
|
||||
title: 'Initiate Renewal',
|
||||
content: `Start renewal process for ${reminder.certNo}?`,
|
||||
onOk: () => {
|
||||
message.success('Renewal process initiated');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string, days: number) => {
|
||||
const config = STATUS_CONFIG[status];
|
||||
if (days < 0) {
|
||||
return <Tag color="error" icon={<CloseCircleOutlined />}>EXPIRED</Tag>;
|
||||
}
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const criticalCount = reminders.filter(r => r.status === 'CRITICAL' || r.daysRemaining < 0).length;
|
||||
const warningCount = reminders.filter(r => r.status === 'WARNING').length;
|
||||
const noticeCount = reminders.filter(r => r.status === 'NOTICE').length;
|
||||
|
||||
return (
|
||||
<div className="certificate-expiry-reminder-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total Certificates"
|
||||
value={reminders.length}
|
||||
prefix={<FileTextOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ background: STATUS_CONFIG.CRITICAL.bgColor }}>
|
||||
<Statistic
|
||||
title="Critical / Expired"
|
||||
value={criticalCount}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ background: STATUS_CONFIG.WARNING.bgColor }}>
|
||||
<Statistic
|
||||
title="Warning"
|
||||
value={warningCount}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card style={{ background: STATUS_CONFIG.NOTICE.bgColor }}>
|
||||
<Statistic
|
||||
title="Notice"
|
||||
value={noticeCount}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<ClockCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{criticalCount > 0 && (
|
||||
<Alert
|
||||
message="Critical Certificates Detected"
|
||||
description={`${criticalCount} certificate(s) have expired or are in critical status. Immediate action required to maintain compliance.`}
|
||||
type="error"
|
||||
showIcon
|
||||
icon={<CloseCircleOutlined />}
|
||||
style={{ marginBottom: 24 }}
|
||||
action={
|
||||
<Button size="small" danger onClick={() => message.info('Navigating to critical certificates...')}>
|
||||
View Details
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card
|
||||
title="Certificate Expiry Reminders"
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<SyncOutlined />} onClick={checkReminders} loading={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button icon={<SettingOutlined />} onClick={() => setSettingsModalVisible(true)}>
|
||||
Settings
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={reminders.sort((a, b) => a.daysRemaining - b.daysRemaining)}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
style={{
|
||||
background: item.daysRemaining < 0 ? '#fff2f0' : item.daysRemaining <= 90 ? '#fffbe6' : 'transparent',
|
||||
padding: 16,
|
||||
marginBottom: 8,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
actions={[
|
||||
<Tooltip title="Send Reminder">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<MailOutlined />}
|
||||
onClick={() => handleSendReminder(item)}
|
||||
disabled={item.reminderSent && item.daysRemaining > 30}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="Initiate Renewal">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<SyncOutlined />}
|
||||
onClick={() => handleInitiateRenewal(item)}
|
||||
disabled={item.status === 'SAFE'}
|
||||
/>
|
||||
</Tooltip>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<Badge
|
||||
count={item.daysRemaining < 0 ? '!' : item.daysRemaining <= 30 ? item.daysRemaining : 0}
|
||||
offset={[-5, 5]}
|
||||
>
|
||||
<FileTextOutlined style={{ fontSize: 32, color: item.daysRemaining < 0 ? '#ff4d4f' : item.daysRemaining <= 90 ? '#faad14' : '#52c41a' }} />
|
||||
</Badge>
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span>{item.certNo}</span>
|
||||
{getStatusTag(item.status, item.daysRemaining)}
|
||||
{item.reminderSent && <Tag icon={<BellOutlined />} color="blue">Reminder Sent</Tag>}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<>
|
||||
<div>{item.certName} - {item.productName}</div>
|
||||
<Space style={{ marginTop: 8 }}>
|
||||
<span>Expiry: {item.expiryDate}</span>
|
||||
<Divider type="vertical" />
|
||||
<span style={{
|
||||
color: item.daysRemaining < 0 ? '#ff4d4f' : item.daysRemaining <= 90 ? '#faad14' : '#52c41a',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{item.daysRemaining < 0
|
||||
? `Expired ${Math.abs(item.daysRemaining)} days ago`
|
||||
: `${item.daysRemaining} days remaining`
|
||||
}
|
||||
</span>
|
||||
</Space>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{item.actions.length > 0 && (
|
||||
<div style={{ marginLeft: 48 }}>
|
||||
<Timeline
|
||||
items={item.actions.map((action, index) => ({
|
||||
color: index === 0 ? 'red' : 'gray',
|
||||
children: action,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Reminder Settings"
|
||||
open={settingsModalVisible}
|
||||
onCancel={() => setSettingsModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setSettingsModalVisible(false)}>Close</Button>,
|
||||
<Button key="save" type="primary" onClick={() => { message.success('Settings saved'); setSettingsModalVisible(false); }}>
|
||||
Save Settings
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>Email Notifications</div>
|
||||
<Switch
|
||||
checked={emailEnabled}
|
||||
onChange={setEmailEnabled}
|
||||
checkedChildren="Enabled"
|
||||
unCheckedChildren="Disabled"
|
||||
/>
|
||||
<div style={{ color: '#666', marginTop: 8 }}>
|
||||
Receive email notifications for certificate expiry reminders
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>Auto-initiate Renewal</div>
|
||||
<Switch
|
||||
checked={autoRenewal}
|
||||
onChange={setAutoRenewal}
|
||||
checkedChildren="Enabled"
|
||||
unCheckedChildren="Disabled"
|
||||
/>
|
||||
<div style={{ color: '#666', marginTop: 8 }}>
|
||||
Automatically initiate renewal process when certificate enters warning status
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider>Reminder Schedule</Divider>
|
||||
|
||||
<List
|
||||
size="small"
|
||||
dataSource={[
|
||||
{ status: 'Critical', timing: '90 days before expiry', frequency: 'Weekly' },
|
||||
{ status: 'Warning', timing: '60 days before expiry', frequency: 'Bi-weekly' },
|
||||
{ status: 'Notice', timing: '30 days before expiry', frequency: 'Weekly' },
|
||||
{ status: 'Final', timing: '7 days before expiry', frequency: 'Daily' },
|
||||
]}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={item.status}
|
||||
description={`${item.timing} - ${item.frequency}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificateExpiryReminder;
|
||||
538
dashboard/src/pages/Compliance/CertificateManage.tsx
Normal file
538
dashboard/src/pages/Compliance/CertificateManage.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
DatePicker,
|
||||
Upload,
|
||||
Descriptions,
|
||||
Divider,
|
||||
message,
|
||||
Tag,
|
||||
Tabs,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Alert,
|
||||
Tooltip,
|
||||
Badge,
|
||||
} from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
PlusOutlined,
|
||||
EyeOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
UploadOutlined,
|
||||
DownloadOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
WarningOutlined,
|
||||
BellOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
interface Certificate {
|
||||
id: string;
|
||||
certNo: string;
|
||||
certName: string;
|
||||
certType: 'CE' | 'FCC' | 'RoHS' | 'UL' | 'ISO' | 'FDA' | 'CCC' | 'OTHER';
|
||||
productId: string;
|
||||
productName: string;
|
||||
issuer: string;
|
||||
issueDate: string;
|
||||
expiryDate: string;
|
||||
status: 'VALID' | 'EXPIRED' | 'PENDING_RENEWAL' | 'REVOKED';
|
||||
attachments: string[];
|
||||
scope: string;
|
||||
remark?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const CERT_TYPES = [
|
||||
{ value: 'CE', label: 'CE Certification', color: 'blue' },
|
||||
{ value: 'FCC', label: 'FCC Certification', color: 'green' },
|
||||
{ value: 'RoHS', label: 'RoHS Compliance', color: 'purple' },
|
||||
{ value: 'UL', label: 'UL Certification', color: 'orange' },
|
||||
{ value: 'ISO', label: 'ISO Certification', color: 'cyan' },
|
||||
{ value: 'FDA', label: 'FDA Approval', color: 'red' },
|
||||
{ value: 'CCC', label: 'CCC Certification', color: 'gold' },
|
||||
{ value: 'OTHER', label: 'Other', color: 'default' },
|
||||
];
|
||||
|
||||
const STATUS_CONFIG: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
VALID: { color: 'success', text: 'Valid', icon: <CheckCircleOutlined /> },
|
||||
EXPIRED: { color: 'error', text: 'Expired', icon: <CloseCircleOutlined /> },
|
||||
PENDING_RENEWAL: { color: 'warning', text: 'Pending Renewal', icon: <ClockCircleOutlined /> },
|
||||
REVOKED: { color: 'red', text: 'Revoked', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const MOCK_PRODUCTS = [
|
||||
{ id: 'P001', name: 'Industrial Sensor A' },
|
||||
{ id: 'P002', name: 'Control Module B' },
|
||||
{ id: 'P003', name: 'Power Supply Unit C' },
|
||||
{ id: 'P004', name: 'Communication Gateway D' },
|
||||
];
|
||||
|
||||
export const CertificateManage: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [certificates, setCertificates] = useState<Certificate[]>([]);
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedCert, setSelectedCert] = useState<Certificate | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
|
||||
useEffect(() => {
|
||||
fetchCertificates();
|
||||
}, []);
|
||||
|
||||
const fetchCertificates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const mockCerts: Certificate[] = [
|
||||
{
|
||||
id: '1',
|
||||
certNo: 'CE-2026-001',
|
||||
certName: 'CE Declaration of Conformity',
|
||||
certType: 'CE',
|
||||
productId: 'P001',
|
||||
productName: 'Industrial Sensor A',
|
||||
issuer: 'TÜV SÜD',
|
||||
issueDate: '2025-01-15',
|
||||
expiryDate: '2027-01-14',
|
||||
status: 'VALID',
|
||||
attachments: ['ce_cert_2026_001.pdf', 'test_report.pdf'],
|
||||
scope: 'Low Voltage Directive (LVD), EMC Directive',
|
||||
createdAt: '2025-01-15 10:00:00',
|
||||
updatedAt: '2025-01-15 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
certNo: 'FCC-2025-045',
|
||||
certName: 'FCC Declaration',
|
||||
certType: 'FCC',
|
||||
productId: 'P002',
|
||||
productName: 'Control Module B',
|
||||
issuer: 'FCC',
|
||||
issueDate: '2024-06-20',
|
||||
expiryDate: '2025-06-19',
|
||||
status: 'PENDING_RENEWAL',
|
||||
attachments: ['fcc_cert.pdf'],
|
||||
scope: 'Part 15B Class B Digital Device',
|
||||
remark: 'Renewal application submitted',
|
||||
createdAt: '2024-06-20 14:30:00',
|
||||
updatedAt: '2025-03-10 09:00:00',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
certNo: 'RoHS-2024-112',
|
||||
certName: 'RoHS Compliance Certificate',
|
||||
certType: 'RoHS',
|
||||
productId: 'P003',
|
||||
productName: 'Power Supply Unit C',
|
||||
issuer: 'SGS',
|
||||
issueDate: '2024-03-01',
|
||||
expiryDate: '2025-02-28',
|
||||
status: 'EXPIRED',
|
||||
attachments: ['rohs_cert.pdf'],
|
||||
scope: 'RoHS 2.0 (2011/65/EU)',
|
||||
remark: 'Needs re-certification',
|
||||
createdAt: '2024-03-01 09:00:00',
|
||||
updatedAt: '2025-03-01 00:00:00',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
certNo: 'UL-2026-008',
|
||||
certName: 'UL Safety Certification',
|
||||
certType: 'UL',
|
||||
productId: 'P004',
|
||||
productName: 'Communication Gateway D',
|
||||
issuer: 'Underwriters Laboratories',
|
||||
issueDate: '2026-02-01',
|
||||
expiryDate: '2027-01-31',
|
||||
status: 'VALID',
|
||||
attachments: ['ul_cert.pdf', 'safety_test.pdf'],
|
||||
scope: 'UL 60950-1, Information Technology Equipment',
|
||||
createdAt: '2026-02-01 11:00:00',
|
||||
updatedAt: '2026-02-01 11:00:00',
|
||||
},
|
||||
];
|
||||
setCertificates(mockCerts);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCert = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const product = MOCK_PRODUCTS.find(p => p.id === values.productId);
|
||||
const newCert: Certificate = {
|
||||
id: `${Date.now()}`,
|
||||
certNo: `${values.certType}-${new Date().getFullYear()}-${String(certificates.length + 1).padStart(3, '0')}`,
|
||||
certName: values.certName,
|
||||
certType: values.certType,
|
||||
productId: values.productId,
|
||||
productName: product?.name || '',
|
||||
issuer: values.issuer,
|
||||
issueDate: values.issueDate.format('YYYY-MM-DD'),
|
||||
expiryDate: values.expiryDate.format('YYYY-MM-DD'),
|
||||
status: 'VALID',
|
||||
attachments: [],
|
||||
scope: values.scope,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setCertificates([newCert, ...certificates]);
|
||||
message.success('Certificate created successfully');
|
||||
setCreateModalVisible(false);
|
||||
form.resetFields();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (cert: Certificate) => {
|
||||
setSelectedCert(cert);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleRenew = (cert: Certificate) => {
|
||||
Modal.confirm({
|
||||
title: 'Renew Certificate',
|
||||
content: `Are you sure you want to initiate renewal for ${cert.certNo}?`,
|
||||
onOk: () => {
|
||||
setCertificates(certificates.map(c =>
|
||||
c.id === cert.id ? { ...c, status: 'PENDING_RENEWAL' as const, updatedAt: new Date().toISOString() } : c
|
||||
));
|
||||
message.success('Renewal initiated');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (certId: string) => {
|
||||
Modal.confirm({
|
||||
title: 'Delete Certificate',
|
||||
content: 'Are you sure you want to delete this certificate?',
|
||||
okType: 'danger',
|
||||
onOk: () => {
|
||||
setCertificates(certificates.filter(c => c.id !== certId));
|
||||
message.success('Certificate deleted');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getFilteredCerts = () => {
|
||||
if (activeTab === 'all') return certificates;
|
||||
if (activeTab === 'expiring') {
|
||||
return certificates.filter(c => {
|
||||
const daysUntilExpiry = dayjs(c.expiryDate).diff(dayjs(), 'day');
|
||||
return daysUntilExpiry > 0 && daysUntilExpiry <= 90;
|
||||
});
|
||||
}
|
||||
return certificates.filter(c => c.status === activeTab.toUpperCase());
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Certificate> = [
|
||||
{
|
||||
title: 'Cert No',
|
||||
dataIndex: 'certNo',
|
||||
key: 'certNo',
|
||||
width: 130,
|
||||
render: (no: string, record: Certificate) => (
|
||||
<a onClick={() => handleViewDetail(record)}>{no}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Certificate Name',
|
||||
dataIndex: 'certName',
|
||||
key: 'certName',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'certType',
|
||||
key: 'certType',
|
||||
width: 100,
|
||||
render: (type: string) => {
|
||||
const config = CERT_TYPES.find(t => t.value === type);
|
||||
return <Tag color={config?.color}>{type}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Product',
|
||||
dataIndex: 'productName',
|
||||
key: 'productName',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: 'Issuer',
|
||||
dataIndex: 'issuer',
|
||||
key: 'issuer',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'Issue Date',
|
||||
dataIndex: 'issueDate',
|
||||
key: 'issueDate',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Expiry Date',
|
||||
dataIndex: 'expiryDate',
|
||||
key: 'expiryDate',
|
||||
width: 100,
|
||||
render: (date: string, record: Certificate) => {
|
||||
const daysUntilExpiry = dayjs(date).diff(dayjs(), 'day');
|
||||
const isExpiringSoon = daysUntilExpiry > 0 && daysUntilExpiry <= 90;
|
||||
return (
|
||||
<Tooltip title={isExpiringSoon ? `Expires in ${daysUntilExpiry} days` : ''}>
|
||||
<span style={{ color: isExpiringSoon || daysUntilExpiry <= 0 ? '#ff4d4f' : undefined }}>
|
||||
{date}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 120,
|
||||
render: (status: string) => {
|
||||
const config = STATUS_CONFIG[status];
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (_, record: Certificate) => (
|
||||
<Space>
|
||||
<Tooltip title="View Details">
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
|
||||
</Tooltip>
|
||||
{record.status === 'VALID' && (
|
||||
<Tooltip title="Renew">
|
||||
<Button type="link" icon={<ClockCircleOutlined />} onClick={() => handleRenew(record)} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title="Delete">
|
||||
<Button type="link" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const stats = {
|
||||
total: certificates.length,
|
||||
valid: certificates.filter(c => c.status === 'VALID').length,
|
||||
expiring: certificates.filter(c => {
|
||||
const days = dayjs(c.expiryDate).diff(dayjs(), 'day');
|
||||
return days > 0 && days <= 90;
|
||||
}).length,
|
||||
expired: certificates.filter(c => c.status === 'EXPIRED').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="certificate-manage-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Total Certificates" value={stats.total} prefix={<FileTextOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Valid" value={stats.valid} valueStyle={{ color: '#52c41a' }} prefix={<CheckCircleOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Expiring Soon" value={stats.expiring} valueStyle={{ color: '#faad14' }} prefix={<WarningOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Expired" value={stats.expired} valueStyle={{ color: '#ff4d4f' }} prefix={<CloseCircleOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Alert
|
||||
message="Certificate Expiry Reminder"
|
||||
description={`${stats.expiring} certificates are expiring within 90 days. Please initiate renewal process.`}
|
||||
type="warning"
|
||||
showIcon
|
||||
icon={<BellOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="Certificate Management" extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateModalVisible(true)}>
|
||||
Add Certificate
|
||||
</Button>
|
||||
}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="All" key="all" />
|
||||
<TabPane tab={<Badge count={stats.expiring} offset={[10, 0]} size="small">Expiring Soon</Badge>} key="expiring" />
|
||||
<TabPane tab="Valid" key="valid" />
|
||||
<TabPane tab="Expired" key="expired" />
|
||||
<TabPane tab="Pending Renewal" key="pending_renewal" />
|
||||
</Tabs>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredCerts()}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Add New Certificate"
|
||||
open={createModalVisible}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
footer={null}
|
||||
width={700}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleCreateCert}>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="certType" label="Certificate Type" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Type">
|
||||
{CERT_TYPES.map(t => (
|
||||
<Option key={t.value} value={t.value}>{t.label}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="productId" label="Product" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Product">
|
||||
{MOCK_PRODUCTS.map(p => (
|
||||
<Option key={p.id} value={p.id}>{p.name}</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="certName" label="Certificate Name" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter certificate name" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="issuer" label="Issuing Authority" rules={[{ required: true }]}>
|
||||
<Input placeholder="Enter issuer name" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="issueDate" label="Issue Date" rules={[{ required: true }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item name="expiryDate" label="Expiry Date" rules={[{ required: true }]}>
|
||||
<DatePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item name="scope" label="Scope">
|
||||
<Input.TextArea rows={3} placeholder="Enter certification scope" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="attachments" label="Attachments">
|
||||
<Upload>
|
||||
<Button icon={<UploadOutlined />}>Upload Files</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={() => setCreateModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading}>Create Certificate</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Certificate Details"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
|
||||
selectedCert?.status === 'VALID' && (
|
||||
<Button key="renew" type="primary" icon={<ClockCircleOutlined />} onClick={() => { handleRenew(selectedCert); setDetailModalVisible(false); }}>
|
||||
Initiate Renewal
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{selectedCert && (
|
||||
<>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="Certificate No">{selectedCert.certNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="Status">
|
||||
<Tag color={STATUS_CONFIG[selectedCert.status].color}>
|
||||
{STATUS_CONFIG[selectedCert.status].text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Certificate Name" span={2}>{selectedCert.certName}</Descriptions.Item>
|
||||
<Descriptions.Item label="Type">
|
||||
<Tag color={CERT_TYPES.find(t => t.value === selectedCert.certType)?.color}>
|
||||
{CERT_TYPES.find(t => t.value === selectedCert.certType)?.label}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Product">{selectedCert.productName}</Descriptions.Item>
|
||||
<Descriptions.Item label="Issuer">{selectedCert.issuer}</Descriptions.Item>
|
||||
<Descriptions.Item label="Issue Date">{selectedCert.issueDate}</Descriptions.Item>
|
||||
<Descriptions.Item label="Expiry Date">{selectedCert.expiryDate}</Descriptions.Item>
|
||||
<Descriptions.Item label="Scope" span={2}>{selectedCert.scope}</Descriptions.Item>
|
||||
{selectedCert.remark && (
|
||||
<Descriptions.Item label="Remark" span={2}>{selectedCert.remark}</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
{selectedCert.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>Attachments</Divider>
|
||||
<Space>
|
||||
{selectedCert.attachments.map((file, index) => (
|
||||
<Button key={index} icon={<DownloadOutlined />} size="small">
|
||||
{file}
|
||||
</Button>
|
||||
))}
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificateManage;
|
||||
508
dashboard/src/pages/Compliance/ComplianceCheck.tsx
Normal file
508
dashboard/src/pages/Compliance/ComplianceCheck.tsx
Normal file
@@ -0,0 +1,508 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
Descriptions,
|
||||
Divider,
|
||||
message,
|
||||
Tag,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Progress,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
SafetyCertificateOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
WarningOutlined,
|
||||
LoadingOutlined,
|
||||
SyncOutlined,
|
||||
FileSearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface ComplianceResult {
|
||||
id: string;
|
||||
productId: string;
|
||||
productName: string;
|
||||
platform: string;
|
||||
checkDate: string;
|
||||
overallStatus: 'PASS' | 'FAIL' | 'WARNING' | 'PENDING';
|
||||
checks: {
|
||||
certCompliance: boolean;
|
||||
labelCompliance: boolean;
|
||||
descriptionCompliance: boolean;
|
||||
imageCompliance: boolean;
|
||||
priceCompliance: boolean;
|
||||
policyCompliance: boolean;
|
||||
};
|
||||
issues: string[];
|
||||
score: number;
|
||||
}
|
||||
|
||||
const PLATFORMS = [
|
||||
{ value: 'Amazon', label: 'Amazon', rules: 15 },
|
||||
{ value: 'eBay', label: 'eBay', rules: 12 },
|
||||
{ value: 'Shopify', label: 'Shopify', rules: 10 },
|
||||
{ value: 'Walmart', label: 'Walmart', rules: 14 },
|
||||
{ value: 'AliExpress', label: 'AliExpress', rules: 11 },
|
||||
];
|
||||
|
||||
const MOCK_PRODUCTS = [
|
||||
{ id: 'P001', name: 'Industrial Sensor A', hasCert: true },
|
||||
{ id: 'P002', name: 'Control Module B', hasCert: true },
|
||||
{ id: 'P003', name: 'Power Supply Unit C', hasCert: false },
|
||||
{ id: 'P004', name: 'Communication Gateway D', hasCert: true },
|
||||
];
|
||||
|
||||
const MOCK_RESULTS: ComplianceResult[] = [
|
||||
{
|
||||
id: '1',
|
||||
productId: 'P001',
|
||||
productName: 'Industrial Sensor A',
|
||||
platform: 'Amazon',
|
||||
checkDate: '2026-03-18 10:30:00',
|
||||
overallStatus: 'PASS',
|
||||
checks: {
|
||||
certCompliance: true,
|
||||
labelCompliance: true,
|
||||
descriptionCompliance: true,
|
||||
imageCompliance: true,
|
||||
priceCompliance: true,
|
||||
policyCompliance: true,
|
||||
},
|
||||
issues: [],
|
||||
score: 100,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
productId: 'P002',
|
||||
productName: 'Control Module B',
|
||||
platform: 'Amazon',
|
||||
checkDate: '2026-03-18 09:15:00',
|
||||
overallStatus: 'WARNING',
|
||||
checks: {
|
||||
certCompliance: true,
|
||||
labelCompliance: true,
|
||||
descriptionCompliance: false,
|
||||
imageCompliance: true,
|
||||
priceCompliance: true,
|
||||
policyCompliance: false,
|
||||
},
|
||||
issues: [
|
||||
'Product description missing warranty information',
|
||||
'Contains restricted keyword in bullet points',
|
||||
],
|
||||
score: 75,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
productId: 'P003',
|
||||
productName: 'Power Supply Unit C',
|
||||
platform: 'eBay',
|
||||
checkDate: '2026-03-17 16:00:00',
|
||||
overallStatus: 'FAIL',
|
||||
checks: {
|
||||
certCompliance: false,
|
||||
labelCompliance: true,
|
||||
descriptionCompliance: false,
|
||||
imageCompliance: false,
|
||||
priceCompliance: true,
|
||||
policyCompliance: true,
|
||||
},
|
||||
issues: [
|
||||
'Missing required CE certification',
|
||||
'Product images missing watermark',
|
||||
'Description contains promotional content',
|
||||
],
|
||||
score: 40,
|
||||
},
|
||||
];
|
||||
|
||||
export const ComplianceCheck: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [results, setResults] = useState<ComplianceResult[]>(MOCK_RESULTS);
|
||||
const [checkModalVisible, setCheckModalVisible] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [selectedResult, setSelectedResult] = useState<ComplianceResult | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
|
||||
const handleRunCheck = async (values: any) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const product = MOCK_PRODUCTS.find(p => p.id === values.productId);
|
||||
const hasCert = product?.hasCert || false;
|
||||
|
||||
const newResult: ComplianceResult = {
|
||||
id: `${Date.now()}`,
|
||||
productId: values.productId,
|
||||
productName: product?.name || '',
|
||||
platform: values.platform,
|
||||
checkDate: new Date().toISOString(),
|
||||
overallStatus: hasCert ? 'PASS' : 'FAIL',
|
||||
checks: {
|
||||
certCompliance: hasCert,
|
||||
labelCompliance: true,
|
||||
descriptionCompliance: hasCert,
|
||||
imageCompliance: true,
|
||||
priceCompliance: true,
|
||||
policyCompliance: hasCert,
|
||||
},
|
||||
issues: hasCert ? [] : ['Missing required certification'],
|
||||
score: hasCert ? 100 : 30,
|
||||
};
|
||||
|
||||
setResults([newResult, ...results]);
|
||||
message.success('Compliance check completed');
|
||||
setCheckModalVisible(false);
|
||||
form.resetFields();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetail = (result: ComplianceResult) => {
|
||||
setSelectedResult(result);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const getStatusConfig = (status: string) => {
|
||||
const configs: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
PASS: { color: 'success', text: 'Pass', icon: <CheckCircleOutlined /> },
|
||||
FAIL: { color: 'error', text: 'Fail', icon: <CloseCircleOutlined /> },
|
||||
WARNING: { color: 'warning', text: 'Warning', icon: <WarningOutlined /> },
|
||||
PENDING: { color: 'processing', text: 'Pending', icon: <LoadingOutlined /> },
|
||||
};
|
||||
return configs[status];
|
||||
};
|
||||
|
||||
const getFilteredResults = () => {
|
||||
if (activeTab === 'all') return results;
|
||||
return results.filter(r => r.overallStatus.toLowerCase() === activeTab);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<ComplianceResult> = [
|
||||
{
|
||||
title: 'Product ID',
|
||||
dataIndex: 'productId',
|
||||
key: 'productId',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: 'Product Name',
|
||||
dataIndex: 'productName',
|
||||
key: 'productName',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
width: 100,
|
||||
render: (platform: string) => <Tag>{platform}</Tag>,
|
||||
},
|
||||
{
|
||||
title: 'Score',
|
||||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 150,
|
||||
render: (score: number, record: ComplianceResult) => (
|
||||
<Tooltip title={`${record.overallStatus}`}>
|
||||
<Progress
|
||||
percent={score}
|
||||
size="small"
|
||||
status={record.overallStatus === 'PASS' ? 'success' : record.overallStatus === 'FAIL' ? 'exception' : 'normal'}
|
||||
strokeColor={record.overallStatus === 'PASS' ? '#52c41a' : record.overallStatus === 'FAIL' ? '#ff4d4f' : '#faad14'}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'overallStatus',
|
||||
key: 'overallStatus',
|
||||
width: 100,
|
||||
render: (status: string) => {
|
||||
const config = getStatusConfig(status);
|
||||
return <Tag color={config.color} icon={config.icon}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Check Date',
|
||||
dataIndex: 'checkDate',
|
||||
key: 'checkDate',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: 'Issues',
|
||||
dataIndex: 'issues',
|
||||
key: 'issues',
|
||||
width: 100,
|
||||
render: (issues: string[]) => issues.length > 0 ? (
|
||||
<Tag color="error">{issues.length} issues</Tag>
|
||||
) : (
|
||||
<Tag color="success">No issues</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
render: (_, record) => (
|
||||
<Button type="link" icon={<FileSearchOutlined />} onClick={() => handleViewDetail(record)}>
|
||||
Details
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const stats = {
|
||||
total: results.length,
|
||||
pass: results.filter(r => r.overallStatus === 'PASS').length,
|
||||
warning: results.filter(r => r.overallStatus === 'WARNING').length,
|
||||
fail: results.filter(r => r.overallStatus === 'FAIL').length,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="compliance-check-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="Total Checks" value={stats.total} prefix={<SafetyCertificateOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Passed"
|
||||
value={stats.pass}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Warnings"
|
||||
value={stats.warning}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={<WarningOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Failed"
|
||||
value={stats.fail}
|
||||
valueStyle={{ color: '#ff4d4f' }}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="Compliance Check Results" extra={
|
||||
<Button type="primary" icon={<SyncOutlined />} onClick={() => setCheckModalVisible(true)}>
|
||||
Run New Check
|
||||
</Button>
|
||||
}>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="All" key="all" />
|
||||
<TabPane tab="Pass" key="pass" />
|
||||
<TabPane tab="Warning" key="warning" />
|
||||
<TabPane tab="Fail" key="fail" />
|
||||
</Tabs>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={getFilteredResults()}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Run Compliance Check"
|
||||
open={checkModalVisible}
|
||||
onCancel={() => setCheckModalVisible(false)}
|
||||
footer={null}
|
||||
width={500}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleRunCheck}>
|
||||
<Form.Item name="productId" label="Product" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Product">
|
||||
{MOCK_PRODUCTS.map(p => (
|
||||
<Option key={p.id} value={p.id}>
|
||||
{p.id} - {p.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="platform" label="Platform" rules={[{ required: true }]}>
|
||||
<Select placeholder="Select Platform">
|
||||
{PLATFORMS.map(p => (
|
||||
<Option key={p.value} value={p.value}>
|
||||
{p.label} ({p.rules} rules)
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message="Compliance Check Scope"
|
||||
description="The check will validate your product against platform-specific rules including certification requirements, labeling standards, description guidelines, image requirements, pricing policies, and platform policies."
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button onClick={() => setCheckModalVisible(false)} style={{ marginRight: 8 }}>Cancel</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loading} icon={<SafetyCertificateOutlined />}>
|
||||
Start Check
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Compliance Check Details"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>Close</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{selectedResult && (
|
||||
<>
|
||||
<Descriptions bordered column={2} style={{ marginBottom: 24 }}>
|
||||
<Descriptions.Item label="Product">{selectedResult.productName}</Descriptions.Item>
|
||||
<Descriptions.Item label="Product ID">{selectedResult.productId}</Descriptions.Item>
|
||||
<Descriptions.Item label="Platform">
|
||||
<Tag>{selectedResult.platform}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Check Date">{selectedResult.checkDate}</Descriptions.Item>
|
||||
<Descriptions.Item label="Overall Status">
|
||||
<Tag color={getStatusConfig(selectedResult.overallStatus).color}>
|
||||
{getStatusConfig(selectedResult.overallStatus).text}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Compliance Score">
|
||||
<Progress
|
||||
percent={selectedResult.score}
|
||||
status={selectedResult.overallStatus === 'PASS' ? 'success' : 'exception'}
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Divider>Compliance Checks</Divider>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Certification Compliance"
|
||||
value={selectedResult.checks.certCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.certCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.certCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Label Compliance"
|
||||
value={selectedResult.checks.labelCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.labelCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.labelCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Description Compliance"
|
||||
value={selectedResult.checks.descriptionCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.descriptionCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.descriptionCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Image Compliance"
|
||||
value={selectedResult.checks.imageCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.imageCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.imageCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Price Compliance"
|
||||
value={selectedResult.checks.priceCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.priceCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.priceCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="Policy Compliance"
|
||||
value={selectedResult.checks.policyCompliance ? 'PASS' : 'FAIL'}
|
||||
valueStyle={{ color: selectedResult.checks.policyCompliance ? '#52c41a' : '#ff4d4f' }}
|
||||
prefix={selectedResult.checks.policyCompliance ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{selectedResult.issues.length > 0 && (
|
||||
<>
|
||||
<Divider>Issues Found</Divider>
|
||||
<Alert
|
||||
type="error"
|
||||
message={`${selectedResult.issues.length} Issues Detected`}
|
||||
description={
|
||||
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
|
||||
{selectedResult.issues.map((issue, index) => (
|
||||
<li key={index}>{issue}</li>
|
||||
))}
|
||||
</ul>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplianceCheck;
|
||||
3
dashboard/src/pages/Compliance/index.ts
Normal file
3
dashboard/src/pages/Compliance/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CertificateManage } from './CertificateManage';
|
||||
export { ComplianceCheck } from './ComplianceCheck';
|
||||
export { CertificateExpiryReminder } from './CertificateExpiryReminder';
|
||||
Reference in New Issue
Block a user