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
This commit is contained in:
2026-03-01 22:49:49 +08:00
parent 0b29fe88b5
commit 2030ebe28f
50 changed files with 8687 additions and 1492 deletions

View File

@@ -1,7 +1,7 @@
/**
* 风险热力图组件
* 风险热力图组件 (V3.1)
*
* 展示受试者 × 表单/访视 矩阵
* 展示受试者 × 事件 矩阵
* - 绿色圆点:通过
* - 黄色图标:警告(可点击)
* - 红色图标:严重(可点击)
@@ -11,7 +11,6 @@
import React from 'react';
import { Spin, Tag, Tooltip } from 'antd';
import {
CheckOutlined,
ExclamationOutlined,
CloseOutlined,
} from '@ant-design/icons';
@@ -24,6 +23,8 @@ interface RiskHeatmapProps {
}
const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading }) => {
const columnLabels = data.columnLabels || {};
const getStatusTag = (status: string) => {
switch (status) {
case 'enrolled':
@@ -39,13 +40,16 @@ const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading })
}
};
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'
@@ -61,7 +65,6 @@ const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading })
);
}
// 通过或待检查:显示小圆点(也可点击)
const dotClass = `qc-heatmap-cell-dot ${cell.status === 'pass' ? 'pass' : 'pending'}`;
const tooltipText = cell.status === 'pass'
? '已通过,点击查看数据'
@@ -83,7 +86,6 @@ const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading })
return (
<div className="qc-heatmap">
{/* 头部 */}
<div className="qc-heatmap-header">
<h3 className="qc-heatmap-title"> (Risk Heatmap)</h3>
<div className="qc-heatmap-legend">
@@ -106,7 +108,6 @@ const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading })
</div>
</div>
{/* 表格内容 */}
<div className="qc-heatmap-body">
<Spin spinning={loading}>
<table className="qc-heatmap-table">
@@ -114,14 +115,16 @@ const RiskHeatmap: React.FC<RiskHeatmapProps> = ({ data, onCellClick, loading })
<tr>
<th className="subject-header"> ID</th>
<th></th>
{data.columns.map((col, idx) => (
<th key={idx}>
{col.split('(')[0]}<br />
<small style={{ fontWeight: 'normal' }}>
{col.includes('(') ? `(${col.split('(')[1]}` : ''}
</small>
</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>