Files
AIclinicalresearch/frontend-v2/src/modules/admin/components/qc-cockpit/RiskHeatmap.tsx
HaHafeng 2030ebe28f feat(iit): Complete V3.1 QC engine + GCP business reports + AI timeline + bug fixes
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
2026-03-01 22:49:49 +08:00

153 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 风险热力图组件 (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;