1384 lines
53 KiB
TypeScript
1384 lines
53 KiB
TypeScript
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]}<br/>cyQty:${v[0]}<br/>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]}<br/>cyQty:${v[0]}<br/>投诉:${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<string, number>()
|
||
const h2 = new Map<string, number>()
|
||
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<string, number>()
|
||
const h2 = new Map<string, number>()
|
||
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<string, AnalysisPageDefinition> = {
|
||
'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<string, number>()
|
||
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<string, number>()
|
||
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]
|
||
}
|