import type { EChartsOption } from 'echarts'
import type { AnalysisContext, AnalysisPageDefinition, ChartDefinition } from '../types/analysis'
import {
buildAeMonthlyByOccurrence,
buildComplaintPareto,
buildComplianceMatching,
buildGlobalMonthlyPpm,
buildInjuryDeviceMatrix,
buildProvinceAeCounts,
buildPsurTopProducts,
buildReportingLagDays,
buildSaeMonthlyByOccurrence,
buildSaeShareMonthly,
buildTopDeviceFailures,
} from '../lib/aggregate'
import {
buildBatchComplaintVsAeCounts,
buildComplaintBatchTop,
buildComplaintCloseCycleDays,
buildComplaintMonthly,
buildCompensationDist,
buildDefectConfirmedProductFault,
buildFaultByRegisterMonthMatrix,
buildHospitalAeVsAdmissionScatter,
buildHospitalComplaintVsAdmissionQty,
buildHospitalProductComplaintRate,
buildInvestigationConclusionDist,
buildIsAeRateByProduct,
buildProductComplaintHalfYearGrowth,
buildProvinceComplaintHalfYearGrowth,
buildTopFaultTypes,
buildTopHospitalsComplaints,
buildTopProductsComplaints,
} from '../lib/quality-aggregate'
import {
COMPLIANCE_DOC_REF,
COMPLIANCE_FOOTNOTE_AUDIT,
COMPLIANCE_FOOTNOTE_PPM,
COMPLIANCE_FOOTNOTE_SHORT,
} from './compliance-baseline'
import { QUALITY_DOC_REF, QLT_ADMISSION_DENOM, QLT_TIME_AXIS } from './quality-baseline'
import {
buildDealerComplaintRatePer1000,
buildDealerOpWrongRatePer1000,
buildOpWrongDealerCounts,
buildOpWrongHospitalTop,
buildProductOpWrongSharePct,
buildProvinceComplaintCounts,
buildProvinceComplaintRatePer1000,
} from '../lib/marketing-aggregate'
import { MARKETING_DOC_REF, MKT_ADMISSION_DENOM, MKT_HOSPITAL_MAP, MKT_TIME_AXIS } from './marketing-baseline'
const MOCK_DATA_SCOPE =
'数据源:前端 fetch 加载 public/mock 下 ae.json、complaint.json、admission.json 的全量记录;未接入 Pinia 全局年份/BU 筛选(上线后与数仓条件对齐)。'
const ex = (extraScope: string, analysisLogic: string) => ({
dataScope: [MOCK_DATA_SCOPE, extraScope].filter(Boolean).join(' '),
analysisLogic,
})
const placeholderPair = (prefix: string, hint = '见主策略对应小节与落地文档指标编码。'): ChartDefinition[] => [
{
id: `${prefix}-1`,
title: '图表位 A(待接数据)',
placeholder: true,
chartExplain: ex('暂无聚合结果。', `占位图表;实现时替换为 optionBuilder,并满足「取数范围 + 分析逻辑」说明要求。${hint}`),
},
{
id: `${prefix}-2`,
title: '图表位 B(待接数据)',
placeholder: true,
chartExplain: ex('暂无聚合结果。', `同上。${hint}`),
},
]
const lineOption = (
title: string,
xAxis: string[],
seriesData: (number | null)[],
seriesName = '事件数',
subtext?: string,
): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: subtext ? 78 : 60, right: 24, bottom: 40, left: 40 },
xAxis: { type: 'category', data: xAxis },
yAxis: { type: 'value' },
series: [{ name: seriesName, type: 'line', smooth: true, data: seriesData }],
})
const barOption = (title: string, labels: string[], values: number[], subtext?: string): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: subtext ? 78 : 60, right: 24, bottom: 60, left: 45 },
xAxis: { type: 'category', data: labels, axisLabel: { rotate: 20 } },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
})
const injuryDeviceHeatmapOption = (saeOnly: boolean, title: string, subtext: string) => {
return (context: AnalysisContext): EChartsOption => {
const { injuries, devices, cells } = buildInjuryDeviceMatrix(context, saeOnly)
const data = cells.map((c) => [devices.indexOf(c.device), injuries.indexOf(c.injury), c.value])
const max = Math.max(1, ...cells.map((c) => c.value))
return {
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { position: 'top' },
grid: { top: 88, height: Math.max(220, injuries.length * 28), left: 80, right: 24, bottom: 48 },
xAxis: { type: 'category', data: devices, splitArea: { show: true } },
yAxis: { type: 'category', data: injuries, splitArea: { show: true } },
visualMap: {
min: 0,
max,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: 4,
inRange: { color: ['#eef2ff', '#312e81'] },
},
series: [
{
name: '报告条数',
type: 'heatmap',
data,
label: { show: true },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } },
},
],
}
}
}
const reportingLagHistogramOption = (context: AnalysisContext): EChartsOption => {
const lags = buildReportingLagDays(context).filter((d) => d >= 0)
const bins = ['0–1', '2–3', '4–5', '6–7', '8–10', '>10']
const counts = [0, 0, 0, 0, 0, 0]
lags.forEach((d) => {
if (d <= 1) counts[0] += 1
else if (d <= 3) counts[1] += 1
else if (d <= 5) counts[2] += 1
else if (d <= 7) counts[3] += 1
else if (d <= 10) counts[4] += 1
else counts[5] += 1
})
return {
title: {
text: '上报滞后天数分布(审核日期 − 登记日期)',
subtext: `${COMPLIANCE_DOC_REF}|CMP-AE-701`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 24, bottom: 40, left: 40 },
xAxis: { type: 'category', data: bins },
yAxis: { type: 'value', name: 'AE 条数' },
series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const complianceAuditSummaryOption = (context: AnalysisContext): EChartsOption => {
const m = buildComplianceMatching(context)
return {
title: {
text: '上报合规审计摘要',
subtext: COMPLIANCE_FOOTNOTE_AUDIT,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72, data: ['占比%'] },
grid: { top: 110, right: 24, bottom: 56, left: 48 },
xAxis: {
type: 'category',
data: ['漏报率', '匹配率', '时效(登记)', '时效(C3)'],
axisLabel: { interval: 0, rotate: 0 },
},
yAxis: { type: 'value', name: '%', max: 100 },
series: [
{
name: '占比%',
type: 'bar',
data: [m.leakRate, m.matchRate, m.timelinessFromRegistrationPct, m.timelinessFromC3Pct],
itemStyle: { borderRadius: [4, 4, 0, 0] },
},
],
}
}
const complianceAuditCountsOption = (context: AnalysisContext): EChartsOption => {
const m = buildComplianceMatching(context)
return {
title: {
text: '应报样本量(投诉 isAe=是)',
subtext: `基准 ${m.flaggedTotal} 条|匹配 ${m.matched}|疑似漏报 ${m.missed}|CMP-CV-101~104`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['36%', '62%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: [
{ name: '已匹配', value: m.matched },
{ name: '疑似漏报', value: m.missed },
],
},
],
}
}
const paretoOption = (context: AnalysisContext): EChartsOption => {
const pareto = buildComplaintPareto(context)
return {
title: { text: '产品×故障 Pareto', left: 'center', textStyle: { fontSize: 14 } },
tooltip: { trigger: 'axis' },
legend: { top: 30, data: ['投诉数', '累计占比%'] },
grid: { top: 70, right: 50, bottom: 70, left: 45 },
xAxis: { type: 'category', data: pareto.labels, axisLabel: { rotate: 25 } },
yAxis: [{ type: 'value', name: '投诉数' }, { type: 'value', name: '累计%', max: 100 }],
series: [
{ name: '投诉数', type: 'bar', data: pareto.bars },
{ name: '累计占比%', type: 'line', yAxisIndex: 1, smooth: true, data: pareto.line },
],
}
}
const faultSeasonalityHeatmapOption = (context: AnalysisContext): EChartsOption => {
const { faults, months, cells } = buildFaultByRegisterMonthMatrix(context)
const max = Math.max(1, ...cells.map((c) => c.value))
const data = cells.map((c) => [months.indexOf(c.month), faults.indexOf(c.fault), c.value])
return {
title: {
text: '故障类型 × C3登记月(投诉热力)',
subtext: `${QUALITY_DOC_REF}|主策略 §5.2 与 QLT-BTH-203 月份桶一致`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { position: 'top' },
grid: { top: 88, height: Math.max(220, faults.length * 26), left: 100, right: 24, bottom: 48 },
xAxis: { type: 'category', data: months, splitArea: { show: true } },
yAxis: { type: 'category', data: faults, splitArea: { show: true } },
visualMap: {
min: 0,
max,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: 4,
inRange: { color: ['#ecfdf5', '#065f46'] },
},
series: [{ name: '投诉条数', type: 'heatmap', data, label: { show: true } }],
}
}
const closeCycleHistogramOption = (context: AnalysisContext): EChartsOption => {
const days = buildComplaintCloseCycleDays(context)
const bins = ['0–7', '8–14', '15–30', '31–60', '>60']
const counts = [0, 0, 0, 0, 0]
days.forEach((d) => {
if (d <= 7) counts[0] += 1
else if (d <= 14) counts[1] += 1
else if (d <= 30) counts[2] += 1
else if (d <= 60) counts[3] += 1
else counts[4] += 1
})
return {
title: {
text: '投诉关闭周期分布',
subtext: 'QLT-RPT-802|关闭日期 − C3登记日期',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 80, right: 24, bottom: 40, left: 44 },
xAxis: { type: 'category', data: bins },
yAxis: { type: 'value', name: '投诉条数' },
series: [{ type: 'bar', data: counts, itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const hospitalAeScatterOption = (context: AnalysisContext): EChartsOption => {
const pts = buildHospitalAeVsAdmissionScatter(context)
const data = pts.map((p) => [p.cyQty, p.aeCount, p.hospital])
return {
title: {
text: '医院:AE 条数 vs 入院 cyQty 汇总',
subtext: 'QLT-AE-1003|AE 按单位名称计数;入院量按医院汇总',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: {
formatter: (params: unknown) => {
const p = params as { value?: [number, number, string] }
const v = p.value
if (!v) return ''
return `${v[2]}
cyQty:${v[0]}
AE:${v[1]}`
},
},
grid: { top: 88, right: 24, bottom: 44, left: 52 },
xAxis: { type: 'value', name: 'cyQty', scale: true },
yAxis: { type: 'value', name: 'AE 条数', scale: true },
series: [{ type: 'scatter', symbolSize: 12, data }],
}
}
const hospitalComplaintQtyScatterOption = (context: AnalysisContext): EChartsOption => {
const pts = buildHospitalComplaintVsAdmissionQty(context)
const data = pts.map((p) => [p.cyQty, p.complaints, p.hospital])
return {
title: {
text: '医院:投诉条数 vs 入院 cyQty',
subtext: QLT_TIME_AXIS,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: {
formatter: (params: unknown) => {
const p = params as { value?: [number, number, string] }
const v = p.value
if (!v) return ''
return `${v[2]}
cyQty:${v[0]}
投诉:${v[1]}`
},
},
grid: { top: 88, right: 24, bottom: 44, left: 52 },
xAxis: { type: 'value', name: 'cyQty', scale: true },
yAxis: { type: 'value', name: '投诉条数', scale: true },
series: [{ type: 'scatter', symbolSize: 12, data }],
}
}
const productHalfYearBarOption = (context: AnalysisContext): EChartsOption => {
const h1 = new Map()
const h2 = new Map()
for (const c of context.complaints) {
const mo = Number.parseInt(c.registerDate.slice(5, 7), 10)
const p = c.productName
if (mo <= 6) h1.set(p, (h1.get(p) ?? 0) + 1)
else h2.set(p, (h2.get(p) ?? 0) + 1)
}
const products = [...new Set([...h1.keys(), ...h2.keys()])].sort()
return {
title: {
text: '产品投诉量:上半年 vs 下半年',
subtext: 'C3登记月份 1–6 vs 7–12|QLT-TRN-703 对比窗演示',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 56, left: 44 },
xAxis: { type: 'category', data: products, axisLabel: { rotate: 22 } },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年(1–6月)', type: 'bar', data: products.map((p) => h1.get(p) ?? 0) },
{ name: '下半年(7–12月)', type: 'bar', data: products.map((p) => h2.get(p) ?? 0) },
],
}
}
const productGrowthAlertBarOption = (context: AnalysisContext): EChartsOption => {
const alertRows = buildProductComplaintHalfYearGrowth(context)
const h1 = new Map()
const h2 = new Map()
for (const c of context.complaints) {
const mo = Number.parseInt(c.registerDate.slice(5, 7), 10)
if (mo <= 6) h1.set(c.productName, (h1.get(c.productName) ?? 0) + 1)
else h2.set(c.productName, (h2.get(c.productName) ?? 0) + 1)
}
const labels = alertRows.length
? alertRows.map((r) => r.product)
: [...new Set(context.complaints.map((c) => c.productName))].sort()
return {
title: {
text: '产品投诉增速预警(下半年相对上半年 ≥20%)',
subtext: alertRows.length ? 'QLT-TRN-703' : 'QLT-TRN-703|当前样本无达阈产品,展示全产品上下半年对照',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 56, left: 44 },
xAxis: { type: 'category', data: labels, axisLabel: { rotate: 22 } },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年', type: 'bar', data: labels.map((p) => h1.get(p) ?? 0) },
{ name: '下半年', type: 'bar', data: labels.map((p) => h2.get(p) ?? 0) },
],
}
}
const provinceHalfYearBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildProvinceComplaintHalfYearGrowth(context)
const labels = rows.map((r) => r.province)
return {
title: {
text: '省份投诉量:上半年 vs 下半年',
subtext: '医院→省映射取自入院量表众数|QLT-TRN-704',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: labels },
yAxis: { type: 'value', name: '投诉条数' },
series: [
{ name: '上半年', type: 'bar', data: rows.map((r) => r.firstHalf) },
{ name: '下半年', type: 'bar', data: rows.map((r) => r.secondHalf) },
],
}
}
const complaintRateBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildHospitalProductComplaintRate(context).slice(0, 10)
const labels = rows.map((r) => `${r.hospital} / ${r.product}`)
return {
title: {
text: '医院×产品 投诉率 Top(每千件入院量)',
subtext: `QLT-SAL-901|${QLT_ADMISSION_DENOM}`,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 24, bottom: 8, left: 8, containLabel: true },
xAxis: { type: 'value', name: '件/千件' },
yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
series: [{ type: 'bar', data: rows.map((r) => r.ratePer1000), itemStyle: { borderRadius: [0, 4, 4, 0] } }],
}
}
const isAeRateBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildIsAeRateByProduct(context)
return {
title: {
text: '按产品的投诉升级率(是否不良事件=是)',
subtext: 'QLT-AE-1001',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 72, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: rows.map((r) => r.product), axisLabel: { rotate: 18 } },
yAxis: { type: 'value', name: '%', max: 100 },
series: [{ type: 'bar', data: rows.map((r) => r.ratePct), itemStyle: { borderRadius: [4, 4, 0, 0] } }],
}
}
const investigationPieOption = (context: AnalysisContext): EChartsOption => {
const dist = buildInvestigationConclusionDist(context)
return {
title: {
text: '调查结论分布',
subtext: 'QLT-INV-501',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['34%', '60%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: dist.map(([name, value]) => ({ name, value })),
},
],
}
}
const compensationPieOption = (context: AnalysisContext): EChartsOption => {
const dist = buildCompensationDist(context)
return {
title: {
text: '赔付结论分布',
subtext: 'QLT-CST-601',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'item' },
series: [
{
type: 'pie',
radius: ['34%', '60%'],
label: { formatter: '{b}: {c} ({d}%)' },
data: dist.map(([name, value]) => ({ name, value })),
},
],
}
}
const defectProductFaultBarOption = (context: AnalysisContext): EChartsOption => {
const d = buildDefectConfirmedProductFault(context)
return barOption('产品缺陷成立:产品×故障 Top', d.labels, d.values, 'QLT-INV-502')
}
const batchComplaintAeBarOption = (context: AnalysisContext): EChartsOption => {
const rows = buildBatchComplaintVsAeCounts(context).filter((r) => r.complaints + r.aeReports > 0)
return {
title: {
text: '批号:投诉条数 vs AE 条数',
subtext: 'QLT-BTH-104|同批号字符串对照(非逐条匹配)',
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
legend: { top: 72 },
grid: { top: 100, right: 24, bottom: 48, left: 44 },
xAxis: { type: 'category', data: rows.map((r) => r.batchNo), axisLabel: { rotate: 18 } },
yAxis: { type: 'value', name: '条数' },
series: [
{ name: '投诉', type: 'bar', data: rows.map((r) => r.complaints) },
{ name: 'AE', type: 'bar', data: rows.map((r) => r.aeReports) },
],
}
}
const complaintMonthlyLineOption = (context: AnalysisContext): EChartsOption => {
const t = buildComplaintMonthly(context)
return lineOption('投诉月度趋势(C3登记月)', t.months, t.values, '投诉条数', `${QUALITY_DOC_REF}|QLT 时间轴`)
}
const marketingHBarOption = (
title: string,
subtext: string,
labels: string[],
values: number[],
xAxisName: string,
): EChartsOption => ({
title: {
text: title,
subtext,
left: 'center',
textStyle: { fontSize: 14 },
subtextStyle: { fontSize: 11, color: '#64748b' },
},
tooltip: { trigger: 'axis' },
grid: { top: 88, right: 28, bottom: 12, left: 12, containLabel: true },
xAxis: { type: 'value', name: xAxisName },
yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
series: [{ type: 'bar', data: values, itemStyle: { borderRadius: [0, 4, 4, 0] } }],
})
const mktOpWrongHospitalOption = (context: AnalysisContext): EChartsOption => {
const d = buildOpWrongHospitalTop(context, 14)
return barOption('操作不当投诉:医院 Top', d.labels, d.values, 'MKT-OP-101')
}
const mktOpWrongDealerCountOption = (context: AnalysisContext): EChartsOption => {
const d = buildOpWrongDealerCounts(context, 14)
return barOption('操作不当投诉:经销商(条数)', d.labels, d.values, 'MKT-OP-102')
}
const mktDealerOpWrongRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildDealerOpWrongRatePer1000(context, 14)
return marketingHBarOption(
'经销商操作不当率(件/千件入院量)',
`MKT-OP-103|${MARKETING_DOC_REF}`,
d.labels,
d.values,
'件/千件',
)
}
const mktProductOpWrongShareOption = (context: AnalysisContext): EChartsOption => {
const d = buildProductOpWrongSharePct(context, 12)
return marketingHBarOption(
'产品:操作不当占该产品投诉比例(%)',
'MKT-OP-104|子集=调查结论为「操作不当」',
d.labels,
d.values,
'%',
)
}
const mktProvinceComplaintRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildProvinceComplaintRatePer1000(context, 16)
return marketingHBarOption(
'省级投诉率(件/千件入院量)',
`MKT-RG-201|${MKT_ADMISSION_DENOM}`,
d.labels,
d.values,
'件/千件',
)
}
const mktDealerComplaintRateOption = (context: AnalysisContext): EChartsOption => {
const d = buildDealerComplaintRatePer1000(context, 16)
return marketingHBarOption(
'经销商投诉率(件/千件入院量)',
`MKT-RG-202|${MARKETING_DOC_REF}`,
d.labels,
d.values,
'件/千件',
)
}
const mktProvinceComplaintCountOption = (context: AnalysisContext): EChartsOption => {
const d = buildProvinceComplaintCounts(context, 14)
return barOption('省级投诉条数 Top(绝对量)', d.labels, d.values, 'MKT-RG-203')
}
const configs: Record = {
'compliance-psur': {
key: 'compliance-psur',
title: 'PSUR 监管分析',
pillar: 'monitor',
strategyRef: '合规方向分析 §2',
subtitle: '监测层:按《合规方向分析》CMP-AE-101/201/301/401/501/601/701 等演示实现。',
sections: [
{
id: 'psur-trend',
title: '总体趋势与暴露',
strategyRefs: ['CMP-AE-101', 'CMP-AE-201', 'CMP-AE-301'],
charts: [
{
id: 'psur-monthly',
title: '不良事件月度趋势(按发生日期)',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表;时间过滤=发生日期 occurrenceDate;统计单元=每条 AE 记录计 1。',
'CMP-AE-101:按公历月对报告条数求和,用于 PSUR「总体趋势」与异常上升监测。',
),
optionBuilder: (context) => {
const trend = buildAeMonthlyByOccurrence(context)
return lineOption(
'CMP-AE-101 不良事件报告数',
trend.months,
trend.values,
'事件数',
`${COMPLIANCE_DOC_REF}|主时间轴=发生日期|单元=报告条数`,
)
},
},
{
id: 'psur-ppm',
title: '全局月度 PPM(入院量 cyQty 分母)',
description: COMPLIANCE_FOOTNOTE_PPM,
chartExplain: ex(
'分子=AE 按发生月计数;分母=入院量表按业务年月汇总 cyQty(演示未按产品拆分全量暴露)。',
'CMP-AE-201:PPM = AE数/入院量×1e6;分母为 0 时在序列中用 null 表示不可算。',
),
optionBuilder: (context) => {
const ppm = buildGlobalMonthlyPpm(context)
return lineOption(
'CMP-AE-201 演示 PPM',
ppm.months,
ppm.ppm,
'PPM',
`${COMPLIANCE_DOC_REF}|分母=入院量 cyQty 按年月汇总`,
)
},
},
{
id: 'psur-device-top',
title: '器械故障表现 Top(占比)',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表 deviceFailure 字段;全量计数。',
'CMP-AE-301:按故障表现归类计数取 Top;tooltip 可扩展占比=该类条数/当期 AE 总数。',
),
optionBuilder: (context) => {
const top = buildTopDeviceFailures(context, 10)
return barOption(
'CMP-AE-301 器械故障 Top',
top.labels,
top.values,
'条数=报告条数;占比见 tooltip',
)
},
},
{
id: 'psur-product-top',
title: '产品事件 Top 分布',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex('不良事件表 productName 全量。', '按产品聚合 AE 报告条数取 Top,用于 PSUR 产品章节与管理驾驶舱分产品线视图。'),
optionBuilder: (context) => {
const top = buildPsurTopProducts(context)
return barOption('产品 AE 条数 Top', top.labels, top.values, COMPLIANCE_DOC_REF)
},
},
],
},
{
id: 'psur-serious',
title: '严重事件、区域与上报时效',
strategyRefs: ['CMP-AE-501', 'CMP-AE-502', 'CMP-AE-601', 'CMP-AE-701'],
charts: [
{
id: 'psur-sae-monthly',
title: 'SAE 报告数(按发生月)',
description: 'SAE:死亡/危及生命/残疾/住院或延长住院 任一(演示字段)',
chartExplain: ex(
'不良事件表;SAE 子集由四个布尔字段任一为真判定(与合规方向分析 §1.4 一致)。',
'CMP-AE-501:在 SAE 子集上按发生月计数,用于严重事件专章趋势。',
),
optionBuilder: (context) => {
const s = buildSaeMonthlyByOccurrence(context)
return lineOption('CMP-AE-501 SAE 条数', s.months, s.values, 'SAE 条数', COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-sae-share',
title: 'SAE 占全部 AE 比例(按发生月)',
description: 'CMP-AE-502',
chartExplain: ex('同月内 SAE 条数与全部 AE 条数之比,时间轴=发生月。', 'CMP-AE-502:观察严重事件是否在总量上升同时占比上升。'),
optionBuilder: (context) => {
const s = buildSaeShareMonthly(context)
return lineOption('CMP-AE-502 SAE%', s.months, s.shares, '占比%', COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-province',
title: '省级 AE 报告数',
description: 'CMP-AE-601',
chartExplain: ex('不良事件表 province 字段;全期按省聚合。', 'CMP-AE-601:省级报告条数分布,用于地理聚集识别(地图可视化可后续接入)。'),
optionBuilder: (context) => {
const p = buildProvinceAeCounts(context)
return barOption('CMP-AE-601 省分布', p.labels, p.values, COMPLIANCE_DOC_REF)
},
},
{
id: 'psur-lag-histogram',
title: '上报滞后天数分布',
description: 'Lag = 审核日期 − 登记日期|CMP-AE-701',
chartExplain: ex(
'AE 表 reviewDate、registrationDate 均可解析的行;Lag=审核日期−登记日期(天)。',
'CMP-AE-701:对滞后天数做直方分箱,支撑上报时效分布与内控阈值讨论。',
),
optionBuilder: reportingLagHistogramOption,
},
],
},
],
},
'compliance-audit': {
key: 'compliance-audit',
title: '上报合规性审计',
pillar: 'diagnose',
strategyRef: '合规方向分析 §3',
subtitle: '诊断层:CMP-CV-101~104 与 CMP-AE-701/702 双口径演示。',
sections: [
{
id: 'audit-core',
title: '审计核心指标',
strategyRefs: ['CMP-CV-101', 'CMP-CV-104', 'CMP-AE-702'],
charts: [
{
id: 'audit-summary-bar',
title: '漏报 / 匹配 / 时效(双口径)',
description: COMPLIANCE_FOOTNOTE_AUDIT,
chartExplain: ex(
'基准=投诉 isAe=true;匹配规则=医院+产品+型号一致且 AE 发生日与 C3登记日相差≤30天。',
'汇总漏报率、匹配率及匹配子集上「审核−登记」「审核−C3」≤15 天占比(演示阈值)。',
),
optionBuilder: complianceAuditSummaryOption,
},
{
id: 'audit-counts-pie',
title: '应报样本结构',
description: '投诉 isAe=是',
chartExplain: ex(
'同上基准集合;在可匹配与疑似漏报二分结构上展示样本量。',
'饼图用于审计抽样框说明:已匹配 vs 需人工复核的疑似漏报。',
),
optionBuilder: complianceAuditCountsOption,
},
],
},
],
},
'quality-concentration': {
key: 'quality-concentration',
title: '投诉集中性与 Pareto',
pillar: 'diagnose',
strategyRef: '质量方向分析 §2–§3',
subtitle: '诊断层:QLT-PRD-401 / QLT-BTH-101 / QLT-CNC 系列(集中性)。',
sections: [
{
id: 'quality-pareto',
title: '集中性与批号',
strategyRefs: ['QLT-PRD-401', 'QLT-BTH-101', 'QLT-CNC-301~303'],
charts: [
{
id: 'quality-pareto-chart',
title: '产品×故障 Pareto(二八)',
description: 'QLT-PRD-401',
chartExplain: ex(
`质量投诉表;${QLT_TIME_AXIS};取 Top8 产品×故障组合。`,
'对「产品名称×故障类型」计数并降序;同时给出累计占比曲线,用于识别贡献约 80% 投诉的组合(Pareto)。',
),
optionBuilder: paretoOption,
},
{
id: 'quality-batch-chart',
title: '批号投诉条数排名',
description: 'QLT-BTH-101',
chartExplain: ex(
'质量投诉表 batchNo 字段;空批号归入「(未填批号)」桶。',
'在统计期内按批号分组对投诉条数计数并取 Top,用于触发批次调查(问题批次识别)。',
),
optionBuilder: (context) => {
const batch = buildComplaintBatchTop(context, 8)
return barOption('批号投诉 Top', batch.labels, batch.values, QUALITY_DOC_REF)
},
},
{
id: 'quality-hospital-top',
title: '医院投诉条数 Top',
description: 'QLT-CNC-301',
chartExplain: ex(
'质量投诉表;按医院名称聚合。',
'统计各医院投诉条数并取 Top,用于识别投诉集中医院(现场调查/培训优先级)。',
),
optionBuilder: (context) => {
const t = buildTopHospitalsComplaints(context, 8)
return barOption('医院投诉 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
{
id: 'quality-product-fault-top',
title: '故障类型 Top',
description: 'QLT-CNC-303',
chartExplain: ex('质量投诉表 faultType 字段。', '按故障类型计数取 Top,与 Pareto 组合图互补的一维缺陷雷达。'),
optionBuilder: (context) => {
const t = buildTopFaultTypes(context, 8)
return barOption('故障类型 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
],
},
],
},
'quality-trends': {
key: 'quality-trends',
title: '趋势分析',
pillar: 'monitor',
strategyRef: '质量方向分析 §2.2 / §4.1',
subtitle: '监测层:月度趋势与上下半年对比、区域对比(QLT-TRN-703/704)。',
sections: [
{
id: 'trend-core',
title: '时间与对比窗',
strategyRefs: ['QLT-TRN-703', 'QLT-TRN-704'],
charts: [
{
id: 'trend-monthly',
title: '投诉月度趋势',
description: 'C3登记月',
chartExplain: ex(
`质量投诉表;${QLT_TIME_AXIS};按公历年月桶聚合。`,
'对每个自然月统计投诉条数,观察总量是否异常上升(与主策略 §3.2 趋势主题一致)。',
),
optionBuilder: complaintMonthlyLineOption,
},
{
id: 'trend-product-half',
title: '产品:上/下半年投诉量',
description: 'QLT-TRN-703',
chartExplain: ex(
'质量投诉表;以登记日期的月份区分上半年(1–6)与下半年(7–12)。',
'按产品对两个半年窗口分别计数,用于与增速预警表对照阅读。',
),
optionBuilder: productHalfYearBarOption,
},
{
id: 'trend-product-alert',
title: '产品投诉增速预警',
description: 'QLT-TRN-703',
chartExplain: ex(
'同上、对比窗为同一公历年的上下半年。',
'当 (下半年−上半年)/上半年 ≥ 20%(且上半年>0)或「上半年零且下半年≥2」时纳入预警柱图。',
),
optionBuilder: productGrowthAlertBarOption,
},
{
id: 'trend-province-half',
title: '省份:上/下半年投诉量',
description: 'QLT-TRN-704',
chartExplain: ex(
'投诉侧按医院映射至省份:映射表由入院量数据中「医院→省」出现频次众数生成。',
'将投诉按登记月拆上下半年,在省级桶内计数,用于区域波动监测。',
),
optionBuilder: provinceHalfYearBarOption,
},
],
},
],
},
'quality-complaint-sales': {
key: 'quality-complaint-sales',
title: '投诉行为:销售与入院关联',
pillar: 'diagnose',
strategyRef: '质量方向分析 §4.3',
subtitle: '诊断层:投诉量与入院量关系、医院×产品投诉率(QLT-SAL-901)。',
sections: [
{
id: 'qc-sales',
title: '入院与投诉率',
strategyRefs: ['QLT-SAL-901', '主策略 3.3 a'],
charts: [
{
id: 'qc-sales-scatter',
title: '医院:投诉条数 vs 入院 cyQty',
description: '散点',
chartExplain: ex(
'投诉按医院计数;入院量表按 HospitalName 汇总 cyQty(跨月相加,演示口径)。',
'用于观察「入院量大是否必然投诉多」;偏离对角带可为异常医院信号(需结合床位/产品结构)。',
),
optionBuilder: hospitalComplaintQtyScatterOption,
},
{
id: 'qc-sales-rate',
title: '医院×产品 投诉率 Top(每千件)',
description: 'QLT-SAL-901',
chartExplain: ex(
`分子=统计期内医院+产品投诉条数;分母=入院量表同键 cyQty 汇总。${QLT_ADMISSION_DENOM}。`,
'投诉率 = 投诉条数 / cyQty × 1000;取 Top10 横向条形图,突出相对暴露下的高频组合。',
),
optionBuilder: complaintRateBarOption,
},
],
},
],
},
'quality-complaint-investigation': {
key: 'quality-complaint-investigation',
title: '投诉行为:调查与赔付关系',
pillar: 'act',
strategyRef: '质量方向分析 §3.3–§3.4',
subtitle: '决策层:调查结论池、缺陷成立组合、赔付结构(QLT-INV / QLT-CST)。',
sections: [
{
id: 'qc-investigation',
title: '调查与成本',
strategyRefs: ['QLT-INV-501', 'QLT-INV-502', 'QLT-CST-601'],
charts: [
{
id: 'qc-inv-pie',
title: '调查结论分布',
description: 'QLT-INV-501',
chartExplain: ex(
'质量投诉表 conclusion(调查结论)字段全量有效行。',
'对结论类别计数并计算占比,观察「产品缺陷成立」等结构是否恶化。',
),
optionBuilder: investigationPieOption,
},
{
id: 'qc-defect-bar',
title: '产品缺陷成立:产品×故障 Top',
description: 'QLT-INV-502',
chartExplain: ex(
'子集:调查结论=「产品缺陷成立」的投诉记录。',
'在子集内按「产品×故障」计数排序,形成已确认质量问题池,供 CAPA/设计变更引用。',
),
optionBuilder: defectProductFaultBarOption,
},
{
id: 'qc-comp-pie',
title: '赔付结论分布',
description: 'QLT-CST-601',
chartExplain: ex('质量投诉表 compensation(赔付结论)字段。', '统计各类赔付方式占比,作为质量失败外显成本(COQ)结构的入口指标。'),
optionBuilder: compensationPieOption,
},
{
id: 'qc-product-top',
title: '产品投诉条数 Top',
description: 'QLT-CNC-302',
chartExplain: ex('质量投诉表 productName。', '按产品聚合投诉条数,与调查/赔付饼图联读以定位重点产品。'),
optionBuilder: (context) => {
const t = buildTopProductsComplaints(context, 8)
return barOption('产品投诉 Top', t.labels, t.values, QUALITY_DOC_REF)
},
},
],
},
],
},
'quality-complaint-batch': {
key: 'quality-complaint-batch',
title: '投诉行为:批次与稳定性',
pillar: 'diagnose',
strategyRef: '质量方向分析 §2',
subtitle: '诊断层:批号与 AE 双通道对照、关闭周期(QLT-BTH-104 / QLT-RPT-802)。',
sections: [
{
id: 'qc-batch',
title: '批次与闭环',
strategyRefs: ['QLT-BTH-104', 'QLT-RPT-802'],
charts: [
{
id: 'qc-batch-ae',
title: '批号:投诉 vs AE 条数',
description: 'QLT-BTH-104',
chartExplain: ex(
'投诉侧 batchNo;AE 侧 productBatch(batchNo);剔除双方均为 0 的批号。',
'同一批号字符串在投诉表 batchNo 与 AE 表 batchNo 上分别计数,用于市场反馈与监管报告双通道对照(非逐条匹配)。',
),
optionBuilder: batchComplaintAeBarOption,
},
{
id: 'qc-close-cycle',
title: '投诉关闭周期分布',
description: 'QLT-RPT-802',
chartExplain: ex(
'仅统计 closeDate、registerDate 均可解析且差分≥0 的投诉行。',
'关闭周期=关闭日期−C3登记日期,按天分段聚合为直方图,用于流程效率与结案 SLA 讨论。',
),
optionBuilder: closeCycleHistogramOption,
},
],
},
],
},
'quality-complaint-text': {
key: 'quality-complaint-text',
title: '投诉行为:文本与流程类',
pillar: 'goal',
strategyRef: '3.3 p-x',
subtitle: '目标层,占位展示文本挖掘与流程效率方向。',
sections: [{ id: 'qc-text', title: '主题组 D', strategyRefs: ['3.3 p-x'], charts: [...placeholderPair('qc-text-a'), ...placeholderPair('qc-text-b')] }],
},
'quality-ae-cause': {
key: 'quality-ae-cause',
title: 'AE 报告原因推测',
pillar: 'diagnose',
strategyRef: '质量方向分析 §4.4',
subtitle: '诊断层:AE 与入院量散点、按产品升级率(QLT-AE-1001/1003)。',
sections: [
{
id: 'ae-cause',
title: '报告行为与升级',
strategyRefs: ['QLT-AE-1003', 'QLT-AE-1001'],
charts: [
{
id: 'ae-scatter-hosp',
title: '医院:AE 条数 vs 入院 cyQty',
description: 'QLT-AE-1003',
chartExplain: ex(
'AE 表按 unitName 计数报告条数;入院表按 HospitalName 汇总 cyQty;AE 未按投诉时间轴过滤。',
'用于发现「相对入院量异常高报告」的医院点;解读需结合合规口径(发生日期)与报告动机,不等同于质量差。',
),
optionBuilder: hospitalAeScatterOption,
},
{
id: 'ae-upgrade-rate',
title: '按产品的投诉升级率',
description: 'QLT-AE-1001',
chartExplain: ex(
'质量投诉表 isAe(是否不良事件)与 productName。',
'升级率 = 该产品 isAe=true 的投诉条数 / 该产品总投诉条数 ×100%,比较各产品线升级意愿差异。',
),
optionBuilder: isAeRateBarOption,
},
],
},
],
},
'quality-ae-only': {
key: 'quality-ae-only',
title: '报告不投诉分析',
pillar: 'monitor',
strategyRef: '3.4.2',
subtitle: '监测层:AE 器械故障分布(演示)。',
sections: [
{
id: 'ae-only',
title: '仅 AE 报告',
strategyRefs: ['3.4.2'],
charts: [
{
id: 'ae-only-failure',
title: 'AE 器械故障表现分布',
description: '演示',
chartExplain: ex(
'不良事件表全量;字段 deviceFailure。',
'对仅走监管通道的报告,按器械故障表现聚类,辅助理解「未并发投诉」的事件形态(主策略 §3.4.2 占位深化方向)。',
),
optionBuilder: (context) => {
const top = buildTopDeviceFailures(context, 8)
return barOption('AE 故障 Top', top.labels, top.values, COMPLIANCE_DOC_REF)
},
},
{
id: 'ae-only-monthly',
title: 'AE 月度条数(发生日期)',
description: '演示',
chartExplain: ex('不良事件表 occurrenceDate 按月桶。', '与投诉月度趋势对照时须在业务上区分两条时间轴。'),
optionBuilder: (context) => {
const t = buildAeMonthlyByOccurrence(context)
return lineOption('AE 月度', t.months, t.values, 'AE 条数', COMPLIANCE_DOC_REF)
},
},
],
},
],
},
'quality-ae-with-complaint': {
key: 'quality-ae-with-complaint',
title: '报告且投诉分析',
pillar: 'diagnose',
strategyRef: '3.4.3',
subtitle: '诊断层:已升级投诉(isAe=true)的故障类型分布(演示)。',
sections: [
{
id: 'ae-with-complaint',
title: '报告+投诉交集',
strategyRefs: ['3.4.3'],
charts: [
{
id: 'ae-with-fault',
title: '已升级投诉的故障类型分布',
description: 'isAe=true',
chartExplain: ex(
'子集:质量投诉表 isAe=true。',
'在「已升级」子集上按 faultType 计数,用于与 AE 表伤害/故障矩阵叙事衔接(细粒度匹配规则见合规审计主题)。',
),
optionBuilder: (context) => {
const sub = context.complaints.filter((c) => c.isAe)
const m = new Map()
sub.forEach((c) => m.set(c.faultType, (m.get(c.faultType) ?? 0) + 1))
const sorted = [...m.entries()].sort((a, b) => b[1] - a[1])
return barOption(
'升级投诉故障类型',
sorted.map((i) => i[0]),
sorted.map((i) => i[1]),
QUALITY_DOC_REF,
)
},
},
{
id: 'ae-with-monthly',
title: '已升级投诉的登记月份趋势',
description: 'C3登记月',
chartExplain: ex('子集:isAe=true;时间字段 registerDate。', '观察升级投诉在登记时间上的分布,用于与 AE 发生月趋势对照。'),
optionBuilder: (context) => {
const sub = context.complaints.filter((c) => c.isAe)
const counter = new Map()
sub.forEach((c) => {
const k = c.registerDate.slice(0, 7)
counter.set(k, (counter.get(k) ?? 0) + 1)
})
const months = [...counter.keys()].sort()
return lineOption('升级投诉月度', months, months.map((x) => counter.get(x) ?? 0), '条数', QUALITY_DOC_REF)
},
},
],
},
],
},
'marketing-operation-training': {
key: 'marketing-operation-training',
title: '操作不当与培训',
pillar: 'act',
strategyRef: '营销方向分析 §2',
subtitle: '决策层:MKT-OP-101~104,支撑医院清单、经销商辅导与培训素材优先级。',
sections: [
{
id: 'marketing-operation',
title: '培训与使用支持',
strategyRefs: ['MKT-OP-101', 'MKT-OP-102', 'MKT-OP-103', 'MKT-OP-104'],
charts: [
{
id: 'mkt-op-hospital',
title: '操作不当投诉:医院分布',
description: 'MKT-OP-101',
chartExplain: ex(
`投诉表子集:调查结论=「操作不当」;${MKT_TIME_AXIS};全量 mock。`,
'按医院名称计数操作不当投诉条数并取 Top,用于院内培训与跟台优先级(主策略 §4.1)。',
),
optionBuilder: mktOpWrongHospitalOption,
},
{
id: 'mkt-op-dealer-count',
title: '操作不当投诉:经销商条数',
description: 'MKT-OP-102',
chartExplain: ex(
`同上子集;${MKT_HOSPITAL_MAP} 将投诉归入经销商后计数。`,
'识别操作不当投诉绝对量高的经销商,作为渠道侧约谈与联合巡院名单输入。',
),
optionBuilder: mktOpWrongDealerCountOption,
},
{
id: 'mkt-op-dealer-rate',
title: '经销商操作不当率(入院量分母)',
description: 'MKT-OP-103',
chartExplain: ex(
`分子=各经销商子集内操作不当投诉条数;分母=入院量表同 DealerName 的 CY Qty 汇总;${MKT_ADMISSION_DENOM}。`,
'率=分子/分母×1000(件/千件),用于发现「相对销量操作不当偏多」的经销商(主策略 §4.1)。',
),
optionBuilder: mktDealerOpWrongRateOption,
},
{
id: 'mkt-op-product-share',
title: '产品:操作不当占比',
description: 'MKT-OP-104',
chartExplain: ex(
'全量投诉按产品计数分母;分子为该产品调查结论=操作不当的条数。',
'占比=操作不当条数/该产品投诉总条数×100%,用于说明书、操作视频与数字化推广素材的迭代优先级。',
),
optionBuilder: mktProductOpWrongShareOption,
},
],
},
],
},
'marketing-region-dealer': {
key: 'marketing-region-dealer',
title: '区域与经销商投诉率',
pillar: 'monitor',
strategyRef: '营销方向分析 §3',
subtitle: '监测层:MKT-RG-201~203,区分相对率与绝对量。',
sections: [
{
id: 'marketing-region',
title: '区域与渠道',
strategyRefs: ['MKT-RG-201', 'MKT-RG-202', 'MKT-RG-203'],
charts: [
{
id: 'mkt-rg-province-rate',
title: '省级投诉率(件/千件入院量)',
description: 'MKT-RG-201',
chartExplain: ex(
`投诉经医院映射至省(${MKT_HOSPITAL_MAP});分母为入院量同省 CY Qty 汇总;${MKT_TIME_AXIS}。`,
'投诉率=投诉条数/入院量×1000,用于区域活动与资源排序(主策略 §4.2)。',
),
optionBuilder: mktProvinceComplaintRateOption,
},
{
id: 'mkt-rg-dealer-rate',
title: '经销商投诉率(件/千件入院量)',
description: 'MKT-RG-202',
chartExplain: ex(
`投诉经医院映射至经销商众数;分母为入院量表 DealerName 维度 CY Qty 汇总。`,
'用于渠道储运、覆盖密度与客户支持资源的再分配假设验证(主策略 §4.2)。',
),
optionBuilder: mktDealerComplaintRateOption,
},
{
id: 'mkt-rg-province-count',
title: '省级投诉条数 Top(绝对量)',
description: 'MKT-RG-203',
chartExplain: ex(
'全量投诉按省计数,不按入院量标准化。',
'与省级投诉率对照,区分「人口/销量大省带来的绝对量大」与「相对率异常高」。',
),
optionBuilder: mktProvinceComplaintCountOption,
},
],
},
],
},
'substance-matrix': {
key: 'substance-matrix',
title: '伤害 × 故障矩阵',
pillar: 'diagnose',
strategyRef: '合规方向分析 §2.4 / 主策略 5.1',
subtitle: '诊断层:CMP-AE-401 全量与 CMP-AE-402 SAE 子集对照。',
sections: [
{
id: 'substance-matrix',
title: '关联矩阵',
strategyRefs: ['CMP-AE-401', 'CMP-AE-402'],
charts: [
{
id: 'matrix-all',
title: '伤害 × 器械故障(全量 AE)',
description: COMPLIANCE_FOOTNOTE_SHORT,
chartExplain: ex(
'不良事件表 injuryExpression × deviceFailure;时间过滤默认全量(可按发生日期窗扩展)。',
'CMP-AE-401:二维计数矩阵,识别高频伤害-故障组合以支撑 CAPA 优先级。',
),
optionBuilder: injuryDeviceHeatmapOption(
false,
'CMP-AE-401 伤害-故障热力图',
`${COMPLIANCE_DOC_REF}|按发生日期纳入全集`,
),
},
{
id: 'matrix-sae',
title: '伤害 × 器械故障(SAE 子集)',
description: 'CMP-AE-402',
chartExplain: ex(
'在 SAE 子集(四严重标准任一)上重复 CMP-AE-401 的矩阵统计。',
'CMP-AE-402:聚焦严重结局组合模式,用于紧急医学评估与对外报告要点。',
),
optionBuilder: injuryDeviceHeatmapOption(
true,
'CMP-AE-402 SAE 子集',
'死亡/危及生命/残疾/住院或延长住院 任一',
),
},
],
},
],
},
'substance-seasonality': {
key: 'substance-seasonality',
title: '故障季节性分析',
pillar: 'monitor',
strategyRef: '主策略 §5.2 / 质量方向分析 §5',
subtitle: '监测层:投诉侧「故障类型×登记月」热力图(与质量方向月度桶一致)。',
sections: [
{
id: 'substance-seasonality',
title: '季节性模式',
strategyRefs: ['5.2', 'QLT-BTH-203'],
charts: [
{
id: 'seasonality-heatmap',
title: '故障类型 × C3登记月',
description: '投诉热力',
chartExplain: ex(
'质量投诉表 faultType × registerDate 公历月份;全量 mock。',
'单元格=该月该故障类型的投诉条数;颜色越深投诉越多,用于观察高温/雨季等是否与某类故障同向波动(探索性,需外部气候数据佐证)。',
),
optionBuilder: faultSeasonalityHeatmapOption,
},
{
id: 'seasonality-monthly-total',
title: '投诉月度总量(对照)',
description: 'QLT 时间轴',
chartExplain: ex('质量投诉表 registerDate 按月聚合。', '给出各月投诉总量曲线,与热力图行合计一致,便于读趋势峰值月份。'),
optionBuilder: complaintMonthlyLineOption,
},
],
},
],
},
'portfolio-persona': {
key: 'portfolio-persona',
title: '安全维度客户画像',
pillar: 'goal',
strategyRef: '6',
subtitle: '目标层,占位展示高投诉高复购客户画像。',
sections: [{ id: 'portfolio-persona', title: '客户画像', strategyRefs: ['6'], charts: placeholderPair('portfolio-persona') }],
},
'portfolio-integrated': {
key: 'portfolio-integrated',
title: '综合联动分析',
pillar: 'act',
strategyRef: '7',
subtitle: '决策层,支撑监管风险热区与优先级矩阵。',
sections: [{ id: 'portfolio-integrated', title: '综合联动', strategyRefs: ['7'], charts: placeholderPair('portfolio-integrated') }],
},
}
export function getAnalysisDefinition(key: string): AnalysisPageDefinition | undefined {
return configs[key]
}