# baoyu-dashboard-update — module recipes

## 产品看板数据更新

产品看板包含以下子模块，数据均从 ERP (`erp销售订单_*.xlsx`) 聚合：

### 产品线营收表 (`productLines`)
- 按 `产品类型#`（Col23）聚合 2024/2025/2026 年度营收
- 核酸扩增原料 / NGS / 提取试剂 / mRNA产品 / 仪器设备 / 其他
- 占比基于总营收计算

### TOP20 主销货号 (`top20skus`)
- 按物料编码聚合2026年1-5月营收，取TOP20
- 每条包含：code / name / ptype / y24 / y25 / y26
- 新增🆕标记逻辑：建立 `new20skus` 的 code Set，在 `renderSkuTopTable()` 中判断 `isNew`

### TOP20 新上市货号 (`new20skus`)
- 首次订单日期≥2025年6月的SKU，按2026营收TOP20
- 每条包含：code / name / ptype / firstDate / y25 / y26
- "首次订单"用 ERP 中该物料编码的最早 `合同日期` (Col1) 近似

### 诊断原料 TOP10 (`diagTop10`)
- 按 `产品类型#` 筛选「核酸扩增原料+提取试剂+mRNA产品+仪器设备」
- 按累计（2024-2026）销售额排序 TOP10
- 每条包含：code / name / rev(万) / custs

### 科研 TOP10 (`researchTop10`)
- 排除诊断原料类型的其他SKU
- 按累计客户数排序 TOP10
- 每条包含：code / name / ptype / rev / custs

---


## 异常预警 + 我的指标 JSON 生成（2026-07-01 新增）

当用户要求"加预警功能""加我的指标""KPI看板""异常提醒"时，生成两个JSON文件供工作台首页fetch。

### 数据流

```
2026年度指标Excel → 人均年度目标
2026客户价格.xlsx → 人均H1实际销售额
2025客户价格.xlsx → 客户H1同比对比
    ↓
Python脚本聚合 → my_kpis.json + alerts.json
    ↓
工作台首页 fetch → 渲染预警面板 + KPI进度表
```

### my_kpis.json 生成

```python
# 1. 读取年度目标（Sheet: 销售拓展部-年度指标--销售额目标 + Sheet1大客户部）
#    列：销售人员 / 年度达成情况 → 年度目标（元）
#    处理合并/转交人员：龙友波→黄文才, 任鑫→陈溪子
annual_targets = {}
for name in rev_sheet + ka_rev_sheet:
    target_str = str(r['年度达成情况']).strip()
    if target_str and target_str != 'nan':
        annual_targets[name] = int(float(target_str))

# 2. 读取实际销售额（2026客户价格.xlsx，过滤价税合计>0）
price26 = pd.read_excel('2026客户价格.xlsx', dtype=str)
price26['amt'] = pd.to_numeric(price26['价税合计'], errors='coerce')
person_actual = price26[price26['amt']>0].groupby('销售员')['amt'].sum()

# 3. 构建人员数组
kpis = []
for name, target in annual_targets.items():
    actual = round(person_actual.get(name, 0))
    pct = round(actual/target*100, 1) if target > 0 else 0
    # 部门：从已知人名映射推导
    dept = '销售拓展部'
    if name in ['张立娅','赵云浩','黄明月']: dept = '大客户部'
    # ...
    kpis.append({
        'name': name, 'dept': dept,
        'annual_target': target, 'actual_ytd': actual,
        'completion_pct': pct,
        'status': 'ok' if pct>=50 else ('warn' if pct>=30 else 'bad')
    })

# 4. 输出到 /数据/my_kpis.json
result = {'period': '2026年1-6月', 'total_target': ..., 'total_actual': ..., 'people': kpis}
```

### alerts.json 生成

```python
# 客户流失预警：H1 同比下滑
for cust, v26 in cust_26.items():
    v25 = cust_25.get(cust, 0)
    if v25 > 50000:  # 仅预警有规模的客户
        if v26 < v25 * 0.5:
            alerts.append({'type':'churn','level':'high', ...})
        elif v26 < v25 * 0.7:
            alerts.append({'type':'churn','level':'medium', ...})

# 销售进度预警：H1完成率<30%
for k in kpis:
    if k['completion_pct'] < 30:
        alerts.append({'type':'revenue','level':'high', ...})

# 新客户预警：v25=0 但 v26>10万
for cust, v26 in cust_26.items():
    if v26 > 100000 and cust not in cust_25:
        alerts.append({'type':'new_customer','level':'info', ...})

# 输出到 /数据/alerts.json，top 20条
```

### 工作台前端集成

在 `renderHome()` 末尾调用 `loadAlerts()` 和 `loadMyKpis()`，fetch JSON后渲染：

- **预警面板**：卡片背景色按level（红/黄/蓝），点击跳转客户页面
- **我的指标表**：销售员/部门/年度目标/H1实际/完成率/进度条，红黄绿三色

### 月度指标处理

月度任务协议书Excel（`大客户/驻地销售/生命科学-月度关键任务协议书-2026.xlsx`）结构复杂（多层表头、合并单元格）。当前阶段仅提取年度销售额目标——这是最可量化的核心KPI。月度定性指标（拜访次数/测试反馈率/NGS客户开发等）留待后续版本。


## 客户维度价格看板（2026-06-30 新增）

在综合工作台的「价格」标签中，提供从客户维度看价格的三视图交互看板。区别于简单的产品目录价列表，此看板分析的是**实际成交价**（从ERP订单的含税单价提取）。

### 数据生成

```python
# 从ERP(16K)提取产品级定价：物料编码 + 含税单价 + 销售数量
erp = pd.read_excel(ERP_FILE, dtype=str)
erp['unit_price'] = pd.to_numeric(erp['含税单价'], errors='coerce')
erp['qty'] = pd.to_numeric(erp['销售数量'], errors='coerce')

# 按 (客户, 物料编码) 分组 → 均价/最低/最高/总数量/年度价格趋势
cust_product = {}
for _, r in erp.iterrows():
    cust = str(r['客户']).strip()
    code = str(r['物料编码']).strip()
    up = float(r['unit_price'])
    yr = str(r['发货日期'])[:4]
    # 聚合 prices, qtys, years, prod_name
    cust_product[cust][code] = {'prices': [...], 'years': [...], ...}

# 输出JSON供工作台fetch
result = {
    'top_customers': [...],  # Top 50客户，含top_products和all_products
    'top_products_by_customer': [...],  # 跨客户比价：每产品列出所有购买客户
}
```

### 三视图设计

**1. 客户总览**（默认）：表格列=客户/集团/采购总额/产品数/均价变动%/Top3产品/详情。均价变动>5%标红（涨价），<-5%标绿（降价）。

**2. 产品比价**：下拉选产品→该产品所有购买客户按均价排列。列=均价/最低/最高/溢价%(vs最低价客户)/采购量/总额/趋势。溢价>10%标红。

**3. 客户明细**：下拉选客户→该客户全部产品采购明细。KPI卡+表格含年度均价数组（如`2024:1.2 2025:1.35 2026:1.5`）。

### HTML集成

- JSON保存到 `/数据/customer_pricing.json`
- 工作台通过 `fetch('/数据/customer_pricing.json')` 加载
- `switchPriceView(v)` 切换三视图，每个视图独立渲染函数
- 替换工作台原有简单价格管理表单，改为完整看板panel


## KPI考核数据更新

### 月度KPI分数 (`KPI_KA_MONTHLY` / `KPI_EXT_MONTHLY`)
- 数据源：`大客户销售部/销售拓展部-销售工程师考核分数通报（2026年X月）.xlsx`
- 每个文件包含1-5月各一个Sheet
- 大客户部：3人（黄明月/赵云浩），得分在"得分"行（通常Row 46+）
- 拓展部：12-13人，得分在"总得分"行
- 使用 `data_only=True` 读取，处理合并单元格
- 趋势列值：首月有数据时计算 △=末月-首月，>5▲ <-5▼ else →

### KPI维度
- 销售拓展部：拜访30% + 测试单20% + 反馈率10% + 回款10% + NGS15% + 逾期15%
- 大客户部：拜访30% + 测试单20% + 反馈率10% + 回款10% + 重点客户15% + 逾期15%
- 评分：T3=120分 T2=100分 T1=60分 低于T1=0分 + 关键任务70% + 部门经理30分

---


## 三年月度对比数据更新

### `data3Y` 对象
- 驾驶舱三年月度同期对比图使用
- 按部门拆分：all / 大客户部 / 销售拓展部 / 生命科学部
- 每条 m24/m25/m26 均为12个月数组（万元），2026年6-12月用null
- 数据优先用部门月度报表（如"大客户部近三年销售数据"Sheet），补全ERP缺失月份
- 图表用 `render3YearChart()` 渲染，chart3Y 变量独立管理（不在 chartInstances 中）

### 注意
- 2024年1-4月ERP数据可能缺失，优先用部门自有的月度报表文件
- 全部部门数据应=∑三个分部门数据（但目前仅大客户部有完整数据）
- **跨文件合并**：主ERP文件(`erp销售订单_*.xlsx`)可能缺少2024年Q1数据。此时需加载`订单分析/2024全年订单.xlsx`单独处理2024年，再合并ERP中的2025-2026。2024文件列结构略有不同（Col0=合同日期, Col3=客户, Col11=价税合计）


## 客户异动分析Tab模式（双年YoY + 批注 + 分级交叉）— 2026-06-27

当用户要求做"2025 vs 2026 客户异动分析"并嵌入仪表盘时：

### 数据流
```
XLS(含2025.1-6 + 2026.1-6 sheet) → 双年聚合 → 
客户变化计算(>1000元阈值) → YOYR_CUSTS数组 →
替换区域同比panel + 新增分级卡片 + 客户明细表
```

### 关键实现

**1. 客户→集团映射（修复大客户部/拓展部列偏移）**
```python
# ⚠️ 关键bug：xlrd读取大客户部/销售拓展部sheet时
# 客户名在col 2，不是col 1！与华北华西等sheet列结构不同
for sn in ["大客户销售部", "销售拓展部"]:
    for i in range(2, ws.nrows):
        grp = str(ws.cell_value(i, 1)).strip()  # col 1 = 集团简称
        cust = str(ws.cell_value(i, 2)).strip()  # col 2 = 客户名称 ← 关键
```

**2. YOYR_CUSTS数据格式**
```javascript
const YOYR_CUSTS=[
  {r:"华北",c:"武汉九泽医疗科技有限公司",g:"九州通集团",
   v25:0,v26:954730,chg:954730,pct:0,s:"🆕"},
  // ... 420+ entries, sorted by |chg| desc
];
// s = "🆕"(新增) | "💀"(流失) | "🔺"(增长) | "🔻"(下降)
```

**3. 分级交叉引用**
```javascript
// 从customerGroups数组提取分级映射
var YOY_GRADES={"丽珠集团":"KA","圣湘集团":"KA",...};

// 分级异动汇总卡片
function renderYoyGrades(){
  // 按KA/A/B/C四级统计净增金额 + 涨跌客户数
  // 输出4个KPI卡片，带颜色编码
}
```

**4. 模糊搜索（中文友好）**
```javascript
// 顺序字符匹配：输入"nk"匹配"纳克智造"
q=q.toLowerCase().replace(/\s+/g,'');
var txt=(c.c+' '+c.g+' '+c.r).toLowerCase();
var j=0,ok=true;
for(var i=0;i<q.length;i++){
  j=txt.indexOf(q[i],j);
  if(j<0){ok=false;break}
  j++;
}
```

**5. 批注实时保存**
```javascript
// oninput触发，立即写localStorage
function saveYoyAnn(el){
  var k=el.getAttribute('data-yoy');
  var v=el.value.trim();
  if(v){yoyAnns[k]=v;el.classList.add('has-note')}
  else{delete yoyAnns[k];el.classList.remove('has-note')}
  localStorage.setItem('yoy_anns',JSON.stringify(yoyAnns));
}
// 状态栏显示：已保存 N 条批注 | 显示 M / 420 条
```

**6. 导出CSV含批注**
```javascript
function exportYoyAnns(){
  // CSV: 区域,客户,集团,级别,2025,2026,变化,状态,批注
}
```

### Panel HTML结构
```
区域同比表(14行) → 分级异动卡片(KA/A/B/C四卡) → 分级依据说明条 → 
客户明细表(搜索+级别筛选+类型筛选+批注列+导出按钮)
```

### 常见陷阱
- 大型JS数组(420条)嵌入后文件从47K→83K，仍在可管理范围
- 批注textarea需要 `oninput` 而非仅 `onblur`，否则用户不点走就丢数据
- 批注恢复时需加 `has-note` CSS类，否则绿色边框不显示
- 模糊搜索要包含客户名+集团名+区域名的拼接文本，提高命中率
- 分级依据说明条放在卡片和表格之间，帮助用户理解KA/A/B/C含义

可复用模式：主表50行年度汇总，点击任意行展开该客户的36个月订单+销售额明细。

**数据准备**：
```python
# 加载集团映射 → 按集团×年×月聚合营收和订单数
grp_data = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
grp_orders = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
# 输出 JS: custTop50[] (年度汇总) + custMonthly{} (月度明细)
```

**HTML结构**：主表列(排名/集团/2024/2025/2026/累计/同比) + `onclick="toggleCustDetail()"`展开行

**展开行渲染**：三组列(2024订单+金额, 2025订单+金额, 2026订单+金额) × 12月 = 36列

**JS关键点**：
- `custMonthly[name].m24[m]` — 月度金额(万)，`>10`万高亮
- `custMonthly[name].o24[m]` — 月度订单数
- `expandedCust` 状态变量控制展开/合拢，`row.querySelector('.cust-monthly-detail')` 动态填充内容
