V3.1 QC Engine: - QcExecutor unified entry + D1-D7 dimension engines + three-level aggregation - HealthScoreEngine + CompletenessEngine + ProtocolDeviationEngine + QcAggregator - B4 flexible cron scheduling (project-level cronExpression + pg-boss dispatcher) - Prisma migrations for qc_field_status, event_status, project_stats GCP Business Reports (Phase A - 4 reports): - D1 Eligibility: record_summary full list + qc_field_status D1 overlay - D2 Completeness: data entry rate and missing rate aggregation - D3/D4 Query Tracking: severity distribution from qc_field_status - D6 Protocol Deviation: D6 dimension filtering - 4 frontend table components + ReportsPage 5-tab restructure AI Timeline Enhancement: - SkillRunner outputs totalRules (33 actual rules vs 1 skill) - iitQcCockpitController severity mapping fix (critical->red, warning->yellow) - AiStreamPage expandable issue detail table with Chinese labels - Event label localization (eventLabel from backend) Business-side One-click Batch QC: - DashboardPage batch QC button with SyncOutlined icon - Auto-refresh QcReport cache after batch execution Bug Fixes: - dimension_code -> rule_category in 4 SQL queries - D1 eligibility data source: record_summary full + qc_field_status overlay - Timezone UTC -> Asia/Shanghai (QcReportService toBeijingTime helper) - Pass rate calculation: passed/totalEvents instead of passed/totalRecords Docs: - Update IIT module status with GCP reports and bug fix milestones - Update system status doc v6.6 with IIT progress Tested: Backend compiles, frontend linter clean, batch QC verified Made-with: Cursor
153 lines
4.6 KiB
TypeScript
153 lines
4.6 KiB
TypeScript
/**
|
||
* 风险热力图组件 (V3.1)
|
||
*
|
||
* 展示受试者 × 事件 矩阵
|
||
* - 绿色圆点:通过
|
||
* - 黄色图标:警告(可点击)
|
||
* - 红色图标:严重(可点击)
|
||
* - 灰色圆点:未开始
|
||
*/
|
||
|
||
import React from 'react';
|
||
import { Spin, Tag, Tooltip } from 'antd';
|
||
import {
|
||
ExclamationOutlined,
|
||
CloseOutlined,
|
||
} from '@ant-design/icons';
|
||
import type { HeatmapData, HeatmapCell } from '../../types/qcCockpit';
|
||
|
||
interface RiskHeatmapProps {
|
||
data: HeatmapData;
|
||
onCellClick: (cell: HeatmapCell) => void;
|
||
loading?: boolean;
|
||
}
|
||
|
||
const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading }) => {
|
||
const columnLabels = data.columnLabels || {};
|
||
|
||
const getStatusTag = (status: string) => {
|
||
switch (status) {
|
||
case 'enrolled':
|
||
return <Tag color="cyan">已入组</Tag>;
|
||
case 'screening':
|
||
return <Tag color="blue">筛查中</Tag>;
|
||
case 'completed':
|
||
return <Tag color="green">已完成</Tag>;
|
||
case 'withdrawn':
|
||
return <Tag color="default">已退出</Tag>;
|
||
default:
|
||
return <Tag>{status}</Tag>;
|
||
}
|
||
};
|
||
|
||
const getColumnLabel = (col: string): string => {
|
||
if (columnLabels[col]) return columnLabels[col];
|
||
if (col.includes('(')) return col.split('(')[0];
|
||
return col;
|
||
};
|
||
|
||
const renderCell = (cell: HeatmapCell) => {
|
||
const hasIssues = cell.status === 'warning' || cell.status === 'fail';
|
||
const handleClick = () => onCellClick(cell);
|
||
|
||
if (hasIssues) {
|
||
const iconClass = `qc-heatmap-cell-icon ${cell.status}`;
|
||
const icon = cell.status === 'fail'
|
||
? <CloseOutlined />
|
||
: <ExclamationOutlined />;
|
||
|
||
return (
|
||
<Tooltip title={`${cell.issueCount} 个问题,点击查看详情`}>
|
||
<div className={iconClass} onClick={handleClick} style={{ cursor: 'pointer' }}>
|
||
{icon}
|
||
</div>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
const dotClass = `qc-heatmap-cell-dot ${cell.status === 'pass' ? 'pass' : 'pending'}`;
|
||
const tooltipText = cell.status === 'pass'
|
||
? '已通过,点击查看数据'
|
||
: '未质控,点击查看数据';
|
||
|
||
return (
|
||
<Tooltip title={tooltipText}>
|
||
<div
|
||
className={dotClass}
|
||
onClick={handleClick}
|
||
style={{
|
||
backgroundColor: cell.status === 'pass' ? '#52c41a' : '#d9d9d9',
|
||
cursor: 'pointer',
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="qc-heatmap">
|
||
<div className="qc-heatmap-header">
|
||
<h3 className="qc-heatmap-title">受试者风险全景图 (Risk Heatmap)</h3>
|
||
<div className="qc-heatmap-legend">
|
||
<span className="qc-heatmap-legend-item">
|
||
<span className="qc-heatmap-legend-dot pass" />
|
||
无异常
|
||
</span>
|
||
<span className="qc-heatmap-legend-item">
|
||
<span className="qc-heatmap-legend-dot warning" />
|
||
疑问 (Query)
|
||
</span>
|
||
<span className="qc-heatmap-legend-item">
|
||
<span className="qc-heatmap-legend-dot fail" />
|
||
严重违规
|
||
</span>
|
||
<span className="qc-heatmap-legend-item">
|
||
<span className="qc-heatmap-legend-dot pending" />
|
||
未开始
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="qc-heatmap-body">
|
||
<Spin spinning={loading}>
|
||
<table className="qc-heatmap-table">
|
||
<thead>
|
||
<tr>
|
||
<th className="subject-header">受试者 ID</th>
|
||
<th>入组状态</th>
|
||
{data.columns.map((col, idx) => {
|
||
const label = getColumnLabel(col);
|
||
return (
|
||
<th key={idx}>
|
||
<Tooltip title={col !== label ? col : undefined}>
|
||
<span>{label}</span>
|
||
</Tooltip>
|
||
</th>
|
||
);
|
||
})}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.rows.map((row, rowIdx) => (
|
||
<tr key={rowIdx}>
|
||
<td className="subject-cell">{row.recordId}</td>
|
||
<td>{getStatusTag(row.status)}</td>
|
||
{row.cells.map((cell, cellIdx) => (
|
||
<td key={cellIdx}>
|
||
<div className="qc-heatmap-cell">
|
||
{renderCell(cell)}
|
||
</div>
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</Spin>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default RiskHeatmap;
|