/* ============================================================
   core.jsx — 数据模型 / 公式引擎 / 应用状态
   口径对齐：科伦《成本共享测算表》(10 表) + 新都《附表1-向车间收集基础数据》
   ============================================================ */
const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext, Fragment } = React;

/* 演示用固定"当前时间"，保证审计记录可复现 */
const NOW = '2026-09-29 10:24';
const TODAY = '2026-09-29';

/* ------------------------------------------------------------------
   1. 产品主数据（测算目录主体 · 取自中台产品信息）
   真实案例：马来酸阿伐曲波帕片 20mg · 新都 J 线
------------------------------------------------------------------ */
const PRODUCT = {
  id: 'XN10100029', base: '1010', name: '马来酸阿伐曲波帕片', spec: '20mg',
  cat: '片剂', packSpec: '10片/板', packQty: 100, unit: '盒', minUnit: '片', minUnitFactor: 10,
  make: '自制', wsCode: '1010212900', wsName: 'J线', wsDesc: 'J线',
};

/* 工艺阶段：半成品（片剂）/ 产成品（包装后，盒） */
const STAGES = [
  { key: 'semi', name: '半成品', unit: '片', factor: 1, per: '万片', seq: 1 },
  { key: 'fin', name: '产成品', unit: '盒', factor: 10, per: '万盒', seq: 2 },
];

/* ------------------------------------------------------------------
   2. 测算工况（生产模式 1-6）—— 数据取自新都《附表1》真实填报值
   每个工况含 半成品/产成品 两段生产数据 + 定员明细 + 降本/设备信息
------------------------------------------------------------------ */
const MODES = [
  {
    key: 'M1', code: 'GK-01', name: '模式1', kind: '目前状态',
    desc: '10万片/批 × 1批 · 单班 8h/天',
    note: '【目前状态-模式1】2.375天生产1批，10万片/批×1批，抽留样120片/批×1批，收率94%，生产时长8小时/天，人员114小时/批',
    batches: 1, hpd: 8,
    saving: '目前状态：2.375天生产1批，10万片/批×1批', savingDue: '',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 100000, sample: null, sampleTimes: 1, yield: 0.95, hours: 14, dailyLabor: 72, water: 5, elec: 4200, steam: 7 },
      fin: { batchQty: 10000, sample: 12, sampleTimes: 1, yield: 0.94, hours: 5, dailyLabor: 42, water: 1.5, elec: 3800, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 0.5, total: 1 },
        { post: '制粒、总混', n: 2, hours: 0.4, total: 0.8 },
        { post: '压片', n: 2, hours: 0.35, total: 0.7 },
        { post: '包衣', n: 2, hours: 0.5, total: 1 },
        { post: '内包装', n: 4, hours: 0.375, total: 1.5 },
        { post: '外包装', n: 4, hours: 0.375, total: 1.5 },
        { post: '车间现场主管', n: 1, hours: 1, total: 1 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
  {
    key: 'M2', code: 'GK-02', name: '模式2', kind: '目前状态',
    desc: '10万片/批 × 5批 · 单班 8h/天',
    note: '【目前状态-模式2】10天生产5批，10万片/批×5批，抽留样120片/批×5批，收率95%，生产时长8小时/天，人员64.4小时/批',
    batches: 5, hpd: 8,
    saving: '目前状态：10天生产5批，10万片/批×5批', savingDue: '',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 100000, sample: null, sampleTimes: 1, yield: 0.96, hours: 62, dailyLabor: 172, water: 5, elec: 4000, steam: 6.5 },
      fin: { batchQty: 10000, sample: 12, sampleTimes: 1, yield: 0.95, hours: 18, dailyLabor: 150, water: 1.5, elec: 3600, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 2, total: 4 },
        { post: '制粒、总混', n: 2, hours: 2, total: 4 },
        { post: '压片', n: 2, hours: 1.75, total: 3.5 },
        { post: '包衣', n: 2, hours: 2, total: 4 },
        { post: '内包装', n: 4, hours: 2, total: 8 },
        { post: '外包装', n: 4, hours: 2, total: 8 },
        { post: '车间现场主管', n: 1, hours: 2, total: 2 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
  {
    key: 'M3', code: 'GK-03', name: '模式3', kind: '扩批生产',
    desc: '50万片/批 × 1批 · 24h/天',
    note: '【扩批生产-模式3】1.75天生产1批，50万片/批×1批，抽留样120片/批×1批，收率95%，生产时长24小时/天，人员222小时/批。扩批生产，计划下达后2个月达成。',
    batches: 1, hpd: 24,
    saving: '扩批生产，计划下达后2个月达成', savingDue: '计划下达后2个月完成',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 500000, sample: null, sampleTimes: 1, yield: 0.96, hours: 28, dailyLabor: 104, water: 5, elec: 4000, steam: 6.5 },
      fin: { batchQty: 50000, sample: 12, sampleTimes: 1, yield: 0.95, hours: 14, dailyLabor: 118, water: 2, elec: 3600, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 0.5, total: 1 },
        { post: '制粒、总混', n: 2, hours: 1, total: 2 },
        { post: '压片', n: 2, hours: 1.5, total: 3 },
        { post: '包衣', n: 2, hours: 0.5, total: 1 },
        { post: '内包装', n: 4, hours: 1.5, total: 6 },
        { post: '外包装', n: 4, hours: 1.5, total: 6 },
        { post: '车间现场主管', n: 1, hours: 2, total: 2 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
  {
    key: 'M4', code: 'GK-04', name: '模式4', kind: '扩批生产',
    desc: '50万片/批 × 5批 · 24h/天',
    note: '【扩批生产-模式4】8.08天生产5批，50万片/批×5批，抽留样120片/批×5批，收率96%，生产时长24小时/天，人员166.8小时/批。扩批生产，计划下达后2个月达成。',
    batches: 5, hpd: 24,
    saving: '扩批生产，计划下达后2个月达成', savingDue: '计划下达后2个月完成',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 500000, sample: null, sampleTimes: 1, yield: 0.97, hours: 136, dailyLabor: 342, water: 4.5, elec: 3800, steam: 6 },
      fin: { batchQty: 50000, sample: 12, sampleTimes: 1, yield: 0.96, hours: 58, dailyLabor: 492, water: 2, elec: 3200, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 2.5, total: 5 },
        { post: '制粒、总混', n: 2, hours: 5, total: 10 },
        { post: '压片', n: 2, hours: 7, total: 14 },
        { post: '包衣', n: 2, hours: 2.5, total: 5 },
        { post: '内包装', n: 4, hours: 7, total: 28 },
        { post: '外包装', n: 4, hours: 7, total: 28 },
        { post: '车间现场主管', n: 1, hours: 7.5, total: 7.5 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
  {
    key: 'M5', code: 'GK-05', name: '模式5', kind: '扩批生产',
    desc: '100万片/批 × 1批 · 24h/天',
    note: '【扩批生产-模式5】3天生产1批，100万片/批×1批，抽留样120片/批×1批，收率96%，生产时长24小时/天，人员358.8小时/批。扩批生产，计划下达后2个月达成，但需求量必须达到100万片才能达到此成本。',
    batches: 1, hpd: 24,
    saving: '扩批生产，计划下达后2个月达成；需求量必须达到100万片才能达到此成本', savingDue: '计划下达后2个月完成',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 1000000, sample: null, sampleTimes: 1, yield: 0.97, hours: 46, dailyLabor: 140.8, water: 5.5, elec: 5300, steam: 8 },
      fin: { batchQty: 100000, sample: 12, sampleTimes: 1, yield: 0.96, hours: 26, dailyLabor: 218, water: 3, elec: 4200, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 0.65, total: 1.3 },
        { post: '制粒、总混', n: 2, hours: 1.75, total: 3.5 },
        { post: '压片', n: 2, hours: 2.5, total: 5 },
        { post: '包衣', n: 2, hours: 0.65, total: 1.3 },
        { post: '内包装', n: 4, hours: 3, total: 12 },
        { post: '外包装', n: 4, hours: 3, total: 12 },
        { post: '车间现场主管', n: 1, hours: 3, total: 3 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
  {
    key: 'M6', code: 'GK-06', name: '模式6', kind: '扩批生产',
    desc: '100万片/批 × 5批 · 24h/天',
    note: '【扩批生产-模式6】14.75天生产5批，100万片/批×5批，抽留样120片/批×5批，收率97%，生产时长24小时/天，人员314小时/批。扩批生产，计划下达后2个月达成，需求量必须达到500万片才能达到此成本。',
    batches: 5, hpd: 24,
    saving: '扩批生产，计划下达后2个月达成；需求量必须达到500万片才能达到此成本', savingDue: '计划下达后2个月完成',
    equip: { amount: 450000, name: '泡罩板检测机', note: '45万元已在2025年预算金额体现' },
    stages: {
      semi: { batchQty: 1000000, sample: null, sampleTimes: 1, yield: 0.975, hours: 222, dailyLabor: 536, water: 5.5, elec: 5100, steam: 8 },
      fin: { batchQty: 100000, sample: 12, sampleTimes: 1, yield: 0.97, hours: 132, dailyLabor: 1034, water: 3, elec: 4000, steam: 0 },
    },
    staff: {
      rows: [
        { post: '粉碎', n: 0, hours: 0, total: 0 },
        { post: '物料平衡', n: 0, hours: 0, total: 0 },
        { post: '称量、预混', n: 2, hours: 3, total: 6 },
        { post: '制粒、总混', n: 2, hours: 8.5, total: 17 },
        { post: '压片', n: 2, hours: 12.5, total: 25 },
        { post: '包衣', n: 2, hours: 3.25, total: 6.5 },
        { post: '内包装', n: 4, hours: 15, total: 60 },
        { post: '外包装', n: 4, hours: 15, total: 60 },
        { post: '车间现场主管', n: 1, hours: 15, total: 15 },
      ],
      changeover: { n: 13, hours: 3.25, total: 6.75, laborH: 54 },
    },
  },
];

const modeById = (key) => MODES.find((m) => m.key === key);

/* ------------------------------------------------------------------
   3. 物料 BOM + 物料价格（科伦《物料BOM》《物料价格》两表）
   原料/辅料按 半成品(片) 投料，包材按 产成品(盒) 投料
   priceTax=含税采购单价(供应部确认)  taxRate=发票税率
   tentativePriceTax=临测含税价（双轨，用于临测价格成本）
   说明：BOM 与单价为演示示例值，可整体替换为真实 BOM
------------------------------------------------------------------ */
const BOM_LINES = [
  { id: 'YL-0001', cat: '原料', stage: 'semi', name: '阿伐曲波帕原料药', spec: '原料药 20mg', unit: 'g', qty: 200, priceTax: 42.00, taxRate: 0.13, supplier: '江苏海慈生物', tentativeSupplier: '浙江海翔药业', tentativePriceTax: 45.20, priceAt: '2026-09-22', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'FZ-0002', cat: '辅料', stage: 'semi', name: '微晶纤维素', spec: 'PH-102', unit: 'g', qty: 400, priceTax: 0.022, taxRate: 0.13, supplier: '安徽山河药用辅料', tentativeSupplier: '同左', tentativePriceTax: 0.022, priceAt: '2026-09-18', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'FZ-0003', cat: '辅料', stage: 'semi', name: '交联聚维酮', spec: 'PVPP XL-10', unit: 'g', qty: 50, priceTax: 0.065, taxRate: 0.13, supplier: '德国巴斯夫（进口）', tentativeSupplier: '同左', tentativePriceTax: 0.065, priceAt: '2026-09-18', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'FZ-0004', cat: '辅料', stage: 'semi', name: '硬脂酸镁', spec: '药用级', unit: 'g', qty: 10, priceTax: 0.018, taxRate: 0.13, supplier: '湖州展望药业', tentativeSupplier: '同左', tentativePriceTax: 0.018, priceAt: '2026-07-31', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'FZ-0005', cat: '辅料', stage: 'semi', name: '薄膜包衣预混剂', spec: '胃溶型 · 橙', unit: 'g', qty: 60, priceTax: 0.14, taxRate: 0.13, supplier: '上海卡乐康', tentativeSupplier: '同左', tentativePriceTax: 0.14, priceAt: '2026-09-15', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'BL-0006', cat: '包材', stage: 'fin', name: '药用铝箔（含PVC）', spec: '泡罩用 · 10片/板', unit: '板', qty: 10000, priceTax: 0.11, taxRate: 0.13, supplier: '江苏中金玛泰', tentativeSupplier: '江苏中金玛泰', tentativePriceTax: 0.13, priceAt: '2026-09-20', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'BL-0007', cat: '包材', stage: 'fin', name: '小盒', spec: '20mg×10片', unit: '个', qty: 10000, priceTax: 0.42, taxRate: 0.13, supplier: '成都万隆包装', tentativeSupplier: '同左', tentativePriceTax: 0.42, priceAt: '2026-09-20', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'BL-0008', cat: '包材', stage: 'fin', name: '说明书', spec: '纸质', unit: '张', qty: 10000, priceTax: 0.03, taxRate: 0.13, supplier: '成都万隆包装', tentativeSupplier: '同左', tentativePriceTax: 0.03, priceAt: '2026-09-20', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
  { id: 'BL-0009', cat: '包材', stage: 'fin', name: '纸箱（含热缩膜）', spec: '100盒/件', unit: '套', qty: 100, priceTax: 3.20, taxRate: 0.13, supplier: '成都万隆包装', tentativeSupplier: '同左', tentativePriceTax: 3.20, priceAt: '2026-09-20', confirmed: true, confirmBy: '供应部', confirmAt: '2026-09-28' },
];

const bomLineById = (id) => BOM_LINES.find((b) => b.id === id);

/* ------------------------------------------------------------------
   4. 费用报工（科伦《费用报工》表 · KL01-KL10，共 10 项）
   报工量：水/电/汽 取自车间填报（单批次×批次），人工取日总人工；
   折旧/物料消耗/检验费/非正常生产费用/其他费用/排污费 按生产时长（小时）报工。
   单价由财务录入。KL01-KL06 为示例单价；KL07-KL10 样表中单价为空，默认 0（不计入成本），
   财务可在「费用报工单价」页直接填写，填写后立即参与核算。
------------------------------------------------------------------ */
const COST_FACTORS = [
  { code: 'KL01', cat: '能耗', name: '水', unit: '吨', wsPrice: 3.80, auxPrice: 1.20, taxRate: 0.09, kind: 'cash', basis: 'water' },
  { code: 'KL02', cat: '能耗', name: '电', unit: '度', wsPrice: 0.75, auxPrice: 0.25, taxRate: 0.13, kind: 'cash', basis: 'elec' },
  { code: 'KL03', cat: '能耗', name: '汽', unit: '吨', wsPrice: 230.00, auxPrice: 60.00, taxRate: 0.09, kind: 'cash', basis: 'steam' },
  { code: 'KL04', cat: '人工', name: '人工', unit: '小时', wsPrice: 32.00, auxPrice: 12.00, taxRate: 0, kind: 'cash', basis: 'labor' },
  { code: 'KL05', cat: '折旧', name: '折旧费', unit: '小时', wsPrice: 180.00, auxPrice: 45.00, taxRate: 0, kind: 'depr', basis: 'hours' },
  { code: 'KL06', cat: '其他', name: '物料消耗', unit: '小时', wsPrice: 15.00, auxPrice: 5.00, taxRate: 0.13, kind: 'cash', basis: 'hours' },
  { code: 'KL07', cat: '其他', name: '检验费', unit: '小时', wsPrice: 0, auxPrice: 0, taxRate: 0, kind: 'cash', basis: 'hours' },
  { code: 'KL08', cat: '折旧', name: '非正常生产费用', unit: '小时', wsPrice: 0, auxPrice: 0, taxRate: 0, kind: 'depr', basis: 'hours' },
  { code: 'KL09', cat: '其他', name: '其他费用', unit: '小时', wsPrice: 0, auxPrice: 0, taxRate: 0, kind: 'cash', basis: 'hours' },
  { code: 'KL10', cat: '其他', name: '排污费', unit: '小时', wsPrice: 0, auxPrice: 0, taxRate: 0, kind: 'cash', basis: 'hours' },
];

/* ------------------------------------------------------------------
   5. 公式引擎（科伦口径）
------------------------------------------------------------------ */
/* 变量表：可插入公式的字段 */
const FIELDS = [
  { id: '批理论产量', src: 'apply', unit: '片/盒' },
  { id: '成品率', src: 'apply', unit: '%' },
  { id: '连续生产批次', src: 'apply', unit: '批' },
  { id: '抽留样', src: 'apply', unit: '盒' },
  { id: '抽样次数', src: 'apply', unit: '次' },
  { id: '标准成品量', src: 'calc', unit: '片/盒' },
  { id: '单一批次成品量', src: 'calc', unit: '片/盒' },
  { id: '生产时长', src: 'apply', unit: 'h' },
  { id: '生产天数', src: 'calc', unit: '天' },
  { id: '单批次人工', src: 'calc', unit: '小时' },
  { id: '报工量', src: 'apply', unit: '吨/度/小时' },
  { id: '含税采购单价', src: 'supply', unit: '元' },
  { id: '发票税率', src: 'supply', unit: '%' },
  { id: '不含税采购单价', src: 'calc', unit: '元' },
  { id: '车间报工单价', src: 'finance', unit: '元' },
  { id: '辅助报工单价', src: 'finance', unit: '元' },
  { id: '物料成本', src: 'calc', unit: '元' },
  { id: '车间付现', src: 'calc', unit: '元' },
  { id: '车间折旧', src: 'calc', unit: '元' },
  { id: '辅助付现', src: 'calc', unit: '元' },
  { id: '辅助折旧', src: 'calc', unit: '元' },
  { id: '测算生产成本', src: 'calc', unit: '元' },
  { id: '临测物料成本', src: 'calc', unit: '元' },
  { id: '临测生产成本', src: 'calc', unit: '元' },
  { id: '最小单位系数', src: 'mdm', unit: '倍' },
  { id: '最小单位测算成本', src: 'calc', unit: '元/最小单位' },
];

/* 公式清单（口径说明 + 锁定状态；公式口径页按此展示） */
const FORMULAS = [
  { id: 'stdQty', name: '标准成品量', locked: true,
    expr: '（批理论产量 × 成品率 − 抽留样 × 抽样次数）× 连续生产批次',
    note: '产成品扣减抽留样；半成品无抽留样不扣减。取自车间填报值。' },
  { id: 'material', name: '物料成本', locked: true,
    expr: 'Σ 物料单耗 × 成品量 × 采购单价（含税 / 不含税双口径）',
    note: '原料辅料按半成品(片)投料、包材按产成品(盒)投料；单价取自物料价格表（供应部确认），另有临测价双轨。' },
  { id: 'wsCash', name: '车间付现', locked: false,
    expr: 'Σ 报工量 × 车间报工单价（水/电/汽/人工/物料消耗）',
    note: '报工量取自车间填报；车间报工单价由财务录入。' },
  { id: 'wsDepr', name: '车间折旧', locked: false,
    expr: '折旧报工量（生产时长）× 车间折旧单价',
    note: 'KL05 折旧费按生产时长（小时）报工。' },
  { id: 'auxCash', name: '辅助付现', locked: false,
    expr: 'Σ 报工量 × 辅助报工单价（水/电/汽/人工/物料消耗）',
    note: '辅助口径：基地公用工程等辅助部门提供，单价由财务录入。' },
  { id: 'auxDepr', name: '辅助折旧', locked: false,
    expr: '折旧报工量（生产时长）× 辅助折旧单价',
    note: '辅助部门折旧按生产时长（小时）报工。' },
  { id: 'prodCost', name: '测算生产成本', locked: false,
    expr: '物料成本 + 车间付现 + 车间折旧 + 辅助付现 + 辅助折旧',
    note: '测算生产成本（含税 / 不含税双口径，另有临测价格口径）。' },
  { id: 'minUnit', name: '最小单位测算成本', locked: false,
    expr: '测算生产成本 ÷ 连续生产总成品量（片）× 最小单位系数',
    note: '最小单位=片；产成品口径按盒（系数 10）换算。' },
  { id: 'tentative', name: '临测价格生产成本', locked: false,
    expr: '临测物料成本 + 车间付现 + 车间折旧 + 辅助付现 + 辅助折旧',
    note: '物料价格按临测价（供应部临测人员维护）双轨测算。' },
];

const SRC_META = {
  mdm: { label: '中台', cls: 'src-bom', dot: 'src-中台' },
  sap: { label: 'SAP', cls: 'src-erp', dot: 'src-SAP' },
  bom: { label: 'BOM', cls: 'src-bom', dot: 'src-BOM' },
  price: { label: '物料价格', cls: 'src-erp', dot: 'src-价格' },
  apply: { label: '车间填报', cls: 'src-app', dot: 'src-车间' },
  supply: { label: '供应部', cls: 'src-erp', dot: 'src-供应' },
  finance: { label: '财务录入', cls: 'src-erp', dot: 'src-财务' },
  calc: { label: '公式计算', cls: 'src-bom', dot: 'src-公式' },
};

/* 不含税价 = 含税价 ÷ (1+税率) */
function notaxPrice(tax, rate) {
  return rate == null || rate === 0 ? tax : tax / (1 + rate);
}

/* 各阶段标准成品量（连续生产口径，含批次数） */
function computeStageStd(m) {
  const semi = m.stages.semi, fin = m.stages.fin;
  const semiStd = semi.batchQty * semi.yield * m.batches;
  const finStd = (fin.batchQty * fin.yield - (fin.sample || 0) * (fin.sampleTimes || 0)) * m.batches;
  return {
    semi: { std: semiStd, perBatch: semiStd / m.batches, days: semi.hours / m.hpd, laborPerBatch: semi.dailyLabor / m.batches },
    fin: { std: finStd, perBatch: finStd / m.batches, days: fin.hours / m.hpd, laborPerBatch: fin.dailyLabor / m.batches },
  };
}

/* 物料成本：Σ 单耗×成品量×单价，含税/不含税 × 正式价/临测价
   bomLines 可传入可编辑的物料清单（默认取 BOM_LINES 常量） */
function computeMaterialCost(m, override, bomLines) {
  const st = computeStageStd(m);
  const tax = { total: 0, lines: [] };
  const noTax = { total: 0, lines: [] };
  const tentTax = { total: 0, lines: [] };
  const tentNoTax = { total: 0, lines: [] };
  (bomLines || BOM_LINES).forEach((b) => {
    const std = b.stage === 'semi' ? st.semi.std : st.fin.std;   /* 片 或 盒 */
    const qty = b.qty / 10000 * std;                              /* 投料量（单耗按 万片/万盒） */
    const pTax = (override && override[b.id] && override[b.id].priceTax != null) ? override[b.id].priceTax : b.priceTax;
    const pTent = (b.tentativePriceTax != null) ? b.tentativePriceTax : pTax;
    const cTax = qty * pTax;
    const cNo = qty * notaxPrice(pTax, b.taxRate);
    const cTent = qty * pTent;
    const cTentNo = qty * notaxPrice(pTent, b.taxRate);
    tax.total += cTax; noTax.total += cNo; tentTax.total += cTent; tentNoTax.total += cTentNo;
    const line = {
      id: b.id, name: b.name, spec: b.spec, unit: b.unit, cat: b.cat, stage: b.stage,
      qty, priceTax: pTax, priceNoTax: notaxPrice(pTax, b.taxRate),
      priceTentTax: pTent, taxRate: b.taxRate, supplier: b.supplier,
      costTax: cTax, costNoTax: cNo, costTentTax: cTent, costTentNoTax: cTentNo,
    };
    tax.lines.push(line); noTax.lines.push(line); tentTax.lines.push(line); tentNoTax.lines.push(line);
  });
  return { tax, noTax, tentTax, tentNoTax, std: st };
}

/* 费用报工成本：报工量 × 车间/辅助单价，付现与折旧分开
   factors 可传入可编辑的报工单价（默认取 COST_FACTORS 常量） */
function computeFactorCost(m, factors) {
  const st = computeStageStd(m);
  const semi = m.stages.semi, fin = m.stages.fin;
  const hours = semi.hours + fin.hours;                          /* 生产时长总（X列已是整段总时长，勿再乘批次） */
  const labor = semi.dailyLabor + fin.dailyLabor;             /* 人工总小时（日总人工合计，Y列即整段总量） */
  const lines = [];
  let wsCash = { tax: 0, noTax: 0 }, wsDepr = { tax: 0, noTax: 0 };
  let auxCash = { tax: 0, noTax: 0 }, auxDepr = { tax: 0, noTax: 0 };
  (factors || COST_FACTORS).forEach((f) => {
    let qty;
    if (f.basis === 'water') qty = (semi.water + fin.water) * m.batches;
    else if (f.basis === 'elec') qty = (semi.elec + fin.elec) * m.batches;
    else if (f.basis === 'steam') qty = (semi.steam + fin.steam) * m.batches;
    else if (f.basis === 'labor') qty = labor;
    else qty = hours;
    const wsTax = qty * f.wsPrice, auxTax = qty * f.auxPrice;
    const wsNo = notaxPrice(wsTax, f.taxRate), auxNo = notaxPrice(auxTax, f.taxRate);
    if (f.kind === 'depr') {
      wsDepr.tax += wsTax; wsDepr.noTax += wsNo; auxDepr.tax += auxTax; auxDepr.noTax += auxNo;
    } else {
      wsCash.tax += wsTax; wsCash.noTax += wsNo; auxCash.tax += auxTax; auxCash.noTax += auxNo;
    }
    lines.push({
      code: f.code, cat: f.cat, name: f.name, unit: f.unit, kind: f.kind, basis: f.basis, qty,
      wsPrice: f.wsPrice, auxPrice: f.auxPrice, taxRate: f.taxRate,
      wsTax, wsNoTax: wsNo, auxTax, auxNoTax: auxNo,
    });
  });
  return { lines, wsCash, wsDepr, auxCash, auxDepr, hours, labor, std: st };
}

/* 成本明细（科伦《成本明细》10 项，不含税口径）
   能耗=水/电/汽，人工=KL04，折旧=KL05+KL08，其他=KL06/KL07/KL09/KL10 */
function computeDetail(mc, fc) {
  const en = (l, codes) => l.filter((x) => codes.indexOf(x.code) >= 0).reduce((s, x) => s + x.wsNoTax, 0);
  const ax = (l, codes) => l.filter((x) => codes.indexOf(x.code) >= 0).reduce((s, x) => s + x.auxNoTax, 0);
  return [
    { k: '测算物料成本', v: mc.noTax.total, src: 'calc' },
    { k: '临测物料成本', v: mc.tentNoTax.total, src: 'calc' },
    { k: '车间能耗', v: en(fc.lines, ['KL01', 'KL02', 'KL03']), src: 'calc' },
    { k: '车间人工', v: en(fc.lines, ['KL04']), src: 'calc' },
    { k: '车间折旧', v: en(fc.lines, ['KL05', 'KL08']), src: 'calc' },
    { k: '车间其他', v: en(fc.lines, ['KL06', 'KL07', 'KL09', 'KL10']), src: 'calc' },
    { k: '辅助能耗', v: ax(fc.lines, ['KL01', 'KL02', 'KL03']), src: 'calc' },
    { k: '辅助人工', v: ax(fc.lines, ['KL04']), src: 'calc' },
    { k: '辅助折旧', v: ax(fc.lines, ['KL05', 'KL08']), src: 'calc' },
    { k: '辅助其他', v: ax(fc.lines, ['KL06', 'KL07', 'KL09', 'KL10']), src: 'calc' },
  ];
}

/* 全链路试算一个工况（bomLines / factors 可传入可编辑数据） */
function computeMode(m, override, bomLines, factors) {
  const mc = computeMaterialCost(m, override, bomLines);
  const fc = computeFactorCost(m, factors);
  const prodCostTax = mc.tax.total + fc.wsCash.tax + fc.wsDepr.tax + fc.auxCash.tax + fc.auxDepr.tax;
  const prodCostNoTax = mc.noTax.total + fc.wsCash.noTax + fc.wsDepr.noTax + fc.auxCash.noTax + fc.auxDepr.noTax;
  const prodCostTentTax = mc.tentTax.total + fc.wsCash.tax + fc.wsDepr.tax + fc.auxCash.tax + fc.auxDepr.tax;
  const prodCostTentNoTax = mc.tentNoTax.total + fc.wsCash.noTax + fc.wsDepr.noTax + fc.auxCash.noTax + fc.auxDepr.noTax;
  const pieces = mc.std.fin.std * PRODUCT.minUnitFactor;   /* 产成品折算成片 */
  return {
    key: m.key, name: m.name, kind: m.kind, desc: m.desc, batches: m.batches,
    std: mc.std, material: mc, factors: fc,
    laborTotal: fc.labor, hoursTotal: fc.hours, changeover: m.staff.changeover.laborH,
    prodCost: { tax: prodCostTax, noTax: prodCostNoTax, tentTax: prodCostTentTax, tentNoTax: prodCostTentNoTax },
    minUnit: {
      piece: { tax: prodCostTax / pieces, noTax: prodCostNoTax / pieces, tentTax: prodCostTentTax / pieces, tentNoTax: prodCostTentNoTax / pieces },
      box: { tax: prodCostTax / mc.std.fin.std, noTax: prodCostNoTax / mc.std.fin.std, tentTax: prodCostTentTax / mc.std.fin.std, tentNoTax: prodCostTentNoTax / mc.std.fin.std },
    },
    detail: computeDetail(mc, fc),
    staffTotal: m.staff.rows.reduce((s, r) => s + r.total, 0),
    staffN: m.staff.rows.reduce((s, r) => s + r.n, 0),
  };
}

/* 把一个药品（含其工况、物料）注册为当前测算对象。
   计算函数默认读全局 PRODUCT / MODES / BOM_LINES，切换或导入药品时整体替换这三个全局量。 */
function applyDrug(drug) {
  if (drug.product) Object.assign(PRODUCT, drug.product);
  if (drug.modes) { MODES.splice(0, MODES.length); drug.modes.forEach((m) => MODES.push(m)); }
  if (drug.bom) { BOM_LINES.splice(0, BOM_LINES.length); drug.bom.forEach((b) => BOM_LINES.push(b)); }
}

/* 按附表1的口径生成一个空白工况，供「新增药品」后由车间填写 */
function blankMode(key, name) {
  const stage = () => ({ batchQty: 0, sample: null, sampleTimes: 1, yield: 1, hours: 0, dailyLabor: 0, water: 0, elec: 0, steam: 0 });
  return {
    key: key, code: key, name: name, kind: '待填写', desc: '待车间填写', note: '',
    batches: 1, hpd: 8, saving: '', savingDue: '',
    equip: { amount: 0, name: '', note: '' },
    stages: { semi: stage(), fin: Object.assign(stage(), { sample: 0 }) },
    staff: { rows: [], changeover: { n: 0, hours: 0, total: 0, laborH: 0 } },
  };
}

function computeAll(override, bomLines, factors) {
  const map = {};
  MODES.forEach((m) => { map[m.key] = computeMode(m, override, bomLines, factors); });
  return map;
}

/* ------------------------------------------------------------------
   6. 测算单（收集任务）与审计留痕
   流程：财务发起 → 车间填报 → 供应部填报 → 财务审批（审核锁定）
------------------------------------------------------------------ */
const STEPS = [
  { key: 'issue', name: '财务发起', dept: '财务部', owner: '财务部' },
  { key: 'prod', name: '车间填报', dept: '生产车间', owner: '生产车间' },
  { key: 'supply', name: '供应部填报', dept: '供应部', owner: '供应部' },
  { key: 'approve', name: '财务审批', dept: '财务部', owner: '财务部' },
];

/* 填报字段（对齐新都附表1 与 科伦物料价格表） */
const STEP_FIELDS = {
  prod: {
    stage: [
      { id: 'batchQty', label: '批理论产量', unit: '片/盒', type: 'number' },
      { id: 'sample', label: '标准单批抽留样', unit: '盒', type: 'number' },
      { id: 'sampleTimes', label: '标准抽样次数', unit: '次', type: 'number' },
      { id: 'yield', label: '成品率', unit: '%', type: 'number', pct: true },
      { id: 'hours', label: '生产时长', unit: 'h', type: 'number' },
      { id: 'dailyLabor', label: '日总人工', unit: '小时', type: 'number' },
      { id: 'water', label: '单批次水', unit: '吨', type: 'number' },
      { id: 'elec', label: '单批次电', unit: '度', type: 'number' },
      { id: 'steam', label: '单批次汽', unit: '吨', type: 'number' },
    ],
    mode: [
      { id: 'batches', label: '连续生产批次', unit: '批', type: 'number' },
      { id: 'hpd', label: '每日生产时长', unit: 'h/天', type: 'number' },
      { id: 'saving', label: '生产条件/降本措施', type: 'text' },
      { id: 'savingDue', label: '预计降本完成时间', type: 'text' },
      { id: 'equipAmount', label: '新增设备金额', unit: '元', type: 'number' },
      { id: 'equipName', label: '新增设备名称', type: 'text' },
    ],
    staff: true,
  },
  supply: {
    fields: [
      { id: 'priceTax', label: '含税采购单价确认', unit: '元', type: 'number' },
      { id: 'tentativePriceTax', label: '临测含税采购价', unit: '元', type: 'number' },
      { id: 'taxRate', label: '发票税率', unit: '%', type: 'number', pct: true },
    ],
  },
};

let _uid = 0;
const uid = (p) => (p || 'id') + '-' + (++_uid);

/* 模式默认值（车间填报表单的初值来源） */
function modeDefaults(m) {
  return {
    batches: m.batches, hpd: m.hpd,
    saving: m.saving, savingDue: m.savingDue,
    equipAmount: m.equip.amount, equipName: m.equip.name,
    semi: Object.assign({}, m.stages.semi),
    fin: Object.assign({}, m.stages.fin),
  };
}

const DECLARATIONS_SEED = [
  {
    id: 'SB-2026-0151', title: '马来酸阿伐曲波帕片 6 工况成本测算（Q4）', period: '2026 Q4',
    product: 'XN10100029', modes: ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'],
    createdAt: '2026-09-26 09:12', creator: '财务部', dept: '财务部', due: '2026-09-30',
    step: 3, status: 'reviewing', priority: 'high',
    progress: { 'M1': 7, 'M2': 7, 'M3': 7, 'M4': 7, 'M5': 7, 'M6': 7 },
    payload: { prod: {}, supply: {} },
    timeline: [
      { t: '2026-09-26 09:12', node: 0, act: '发起成本测算', who: '财务部', dept: '财务部', ok: true, note: '本期新增扩批生产模式3-6，需车间按附表1口径填报，供应部确认物料价格。', changes: [] },
      { t: '2026-09-26 09:14', node: 0, act: '通知生产车间 / 供应部', who: '系统', dept: '—', ok: true, note: '已推送至 J线车间（生产车间）与供应部（供应部）。', changes: [] },
      { t: '2026-09-27 15:40', node: 1, act: '车间提交填报', who: '生产车间', dept: '生产车间', ok: true, note: '补充扩批生产模式3-6：计划下达后2个月达成；新增泡罩板检测机 45 万元（已在2025年预算体现）。', changes: [
        { product: 'M4', k: '半成品 · 成品率', from: '96.0 %', to: '97.0 %' },
        { product: 'M6', k: '半成品 · 成品率', from: '97.0 %', to: '97.5 %' },
        { product: 'M3-M6', k: '新增设备', from: '—', to: '泡罩板检测机 45 万元' },
      ] },
      { t: '2026-09-28 11:05', node: 2, act: '供应部提交填报', who: '供应部', dept: '供应部', ok: true, note: '9 项物料含税单价已与 9 月供应商报价核对；原料药与铝箔存在临测价差异待财务复核。', changes: [
        { product: '全局', k: '含税采购单价', from: '9 项待确认', to: '9 项已确认' },
        { product: 'YL-0001', k: '临测含税价', from: '—', to: '45.20 元/g' },
      ] },
      { t: '2026-09-28 11:06', node: 3, act: '流转至财务审批', who: '系统', dept: '—', ok: true, note: '等待财务复核，通过后成本明细将审核锁定。', changes: [] },
    ],
  },
  {
    id: 'SB-2026-0152', title: '模式5/6 扩批成本测算复核（Q4 补充）', period: '2026 Q4',
    product: 'XN10100029', modes: ['M5', 'M6'],
    createdAt: '2026-09-26 09:20', creator: '财务部', dept: '财务部', due: '2026-10-08',
    step: 1, status: 'pending', priority: 'normal',
    progress: { 'M5': 4, 'M6': 3 },
    payload: { prod: {}, supply: {} },
    timeline: [
      { t: '2026-09-26 09:20', node: 0, act: '发起成本测算', who: '财务部', dept: '财务部', ok: true, note: '模式5/6 扩批生产数据待车间确认，供应部需补充临测价。', changes: [] },
      { t: '2026-09-26 09:21', node: 1, act: '通知生产车间', who: '系统', dept: '—', ok: true, note: '已推送至 J线车间。', changes: [] },
    ],
  },
  {
    id: 'SB-2026-0150', title: '马来酸阿伐曲波帕片 Q3 成本测算（基线）', period: '2026 Q3',
    product: 'XN10100029', modes: ['M1', 'M2'],
    createdAt: '2026-06-24 10:02', creator: '财务部', dept: '财务部', due: '2026-06-30',
    step: 4, status: 'approved', priority: 'normal',
    lockedAt: '2026-06-27 10:20', lockBy: '财务部',
    progress: { 'M1': 7, 'M2': 7 },
    payload: { prod: {}, supply: {} },
    timeline: [
      { t: '2026-06-24 10:02', node: 0, act: '发起成本测算', who: '财务部', dept: '财务部', ok: true, note: '', changes: [] },
      { t: '2026-06-25 14:10', node: 1, act: '车间提交填报', who: '生产车间', dept: '生产车间', ok: true, note: '', changes: [] },
      { t: '2026-06-26 09:35', node: 2, act: '供应部提交填报', who: '供应部', dept: '供应部', ok: true, note: '', changes: [] },
      { t: '2026-06-27 10:20', node: 3, act: '财务审批通过 · 审核锁定', who: '财务部', dept: '财务部', ok: true, note: '数据齐备，已归档为 Q3 核算基线，成本明细已锁定。', changes: [] },
    ],
  },
  {
    id: 'SB-2026-0149', title: '马来酸阿伐曲波帕片 Q2 成本测算（已驳回）', period: '2026 Q2',
    product: 'XN10100029', modes: ['M1', 'M2'],
    createdAt: '2026-03-24 10:06', creator: '财务部', dept: '财务部', due: '2026-03-30',
    step: 1, status: 'rejected', priority: 'normal',
    progress: { 'M1': 5, 'M2': 5 },
    payload: { prod: {}, supply: {} },
    timeline: [
      { t: '2026-03-24 10:06', node: 0, act: '发起成本测算', who: '财务部', dept: '财务部', ok: true, note: '', changes: [] },
      { t: '2026-03-25 14:20', node: 1, act: '车间提交填报', who: '生产车间', dept: '生产车间', ok: true, note: '', changes: [
        { product: 'M1', k: '半成品 · 成品率', from: '96.0 %', to: '93.5 %' },
      ] },
      { t: '2026-03-26 16:02', node: 3, act: '财务驳回', who: '财务部', dept: '财务部', ok: false, note: '成品率由 96% 降至 93.5%，与生产日报不符，请附批次记录说明。驳回至车间填报节点。', changes: [
        { product: 'M1', k: '半成品 · 成品率', from: '93.5 %', to: '96.0 %（待举证）' },
      ] },
    ],
  },
];

/* 主数据同步（中台 / SAP / BOM / 物料价格库） */
const SYNC_SEED = {
  sources: [
    { id: 'MDM', name: '中台产品信息', kind: '产品主数据', mode: '定时 · 每日 02:00', last: '2026-09-29 02:00', dur: '6s', objects: 1, fields: 15, status: 'ok', health: 100 },
    { id: 'SAP', name: 'SAP 制造主数据', kind: '制造分类 / 车间', mode: '定时 · 每 6 小时', last: '2026-09-29 06:00', dur: '12s', objects: 3, fields: 42, status: 'ok', health: 100 },
    { id: 'BOM', name: 'BOM 系统', kind: '物料单耗 / 投料量', mode: '定时 · 每日 06:00', last: '2026-09-29 06:00', dur: '9s', objects: 9, fields: 60, status: 'ok', health: 100 },
    { id: 'PRICE', name: '物料价格库', kind: '含税单价 / 临测价', mode: '定时 · 每日 02:00', last: '2026-09-29 02:00', dur: '38s', objects: 9, fields: 45, status: 'warn', health: 87 },
  ],
  runs: [
    { id: 'RUN-9821', t: '2026-09-29 06:00', src: 'BOM', result: 'ok', add: 0, upd: 0, del: 0, skip: 60, dur: '9s', by: '定时' },
    { id: 'RUN-9820', t: '2026-09-29 02:00', src: 'PRICE', result: 'drift', add: 0, upd: 2, del: 0, skip: 43, dur: '38s', by: '定时' },
    { id: 'RUN-9819', t: '2026-09-28 14:20', src: 'PRICE', result: 'ok', add: 0, upd: 0, del: 0, skip: 45, dur: '35s', by: '供应部（手动）' },
    { id: 'RUN-9818', t: '2026-09-28 06:00', src: 'BOM', result: 'ok', add: 0, upd: 0, del: 0, skip: 60, dur: '8s', by: '定时' },
    { id: 'RUN-9817', t: '2026-09-27 02:00', src: 'PRICE', result: 'fail', add: 0, upd: 0, del: 0, skip: 0, dur: '6s', by: '定时', err: '价格库接口 504 超时，已自动重试 3 次后放弃' },
    { id: 'RUN-9816', t: '2026-09-26 02:00', src: 'PRICE', result: 'ok', add: 1, upd: 0, del: 0, skip: 44, dur: '42s', by: '定时' },
  ],
  /* 差异：待确认项 */
  diffs: [
    { id: 'DF-01', mid: 'YL-0001', field: '含税采购单价', local: 42.00, remote: 43.50, unit: '元/g', from: 'ERP 采购订单 PO-20260926-08', status: 'open', impact: ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'] },
    { id: 'DF-02', mid: 'BL-0006', field: '含税采购单价', local: 0.11, remote: 0.13, unit: '元/板', from: '供应部供应商报价单 202609', status: 'open', impact: ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'] },
    { id: 'DF-03', mid: 'FZ-0004', field: '含税采购单价', local: 0.018, remote: 0.018, unit: '元/g', from: '物料价格库', status: 'stale', staleDays: 60, impact: ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'] },
    { id: 'DF-04', mid: 'FZ-0002', field: '临测含税采购价', local: 0.022, remote: 0.021, unit: '元/g', from: '供应部供应商报价 202609', status: 'resolved', impact: ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'] },
  ],
  /* 字段映射 */
  mapping: [
    { local: '产品编码', remote: 'MDM.PROD_CODE', type: 'string', key: true, on: true },
    { local: '装量规格', remote: 'MDM.PACK_SPEC', type: 'string', key: false, on: true },
    { local: '件装量', remote: 'MDM.PACK_QTY', type: 'int', key: false, on: true },
    { local: '最小单位系数', remote: 'MDM.MIN_UNIT_FACTOR', type: 'decimal(2)', key: false, on: true },
    { local: '制造分类', remote: 'SAP.MAKE_TYPE', type: 'string', key: false, on: true },
    { local: '生产车间', remote: 'SAP.WS_CODE', type: 'string', key: false, on: true },
    { local: '物料编码', remote: 'BOM.MAT_CODE', type: 'string', key: true, on: true },
    { local: '物料单耗量', remote: 'BOM.QTY_PER_UNIT', type: 'decimal(3)', key: false, on: true },
    { local: '含税采购单价', remote: 'PRICE.TAX_PRICE', type: 'decimal(4)', key: false, on: true },
    { local: '发票税率', remote: 'PRICE.TAX_RATE', type: 'decimal(2)', key: false, on: true },
    { local: '临测含税采购价', remote: 'PRICE.TENT_PRICE', type: 'decimal(4)', key: false, on: true },
    { local: '车间报工单价', remote: 'FIN.WS_UNIT_PRICE', type: 'decimal(2)', key: false, on: false },
    { local: '辅助报工单价', remote: 'FIN.AUX_UNIT_PRICE', type: 'decimal(2)', key: false, on: false },
  ],
};

/* 全局审计流（由测算单时间线 + 同步事件合成） */
function buildAuditLog(decls, sync) {
  const rows = [];
  decls.forEach((d) => d.timeline.forEach((e) => rows.push({
    id: d.id + '|' + e.t, t: e.t, mod: '成本测算', ref: d.id, refTitle: d.title,
    act: e.act, who: e.who, dept: e.dept, ok: e.ok, note: e.note, changes: e.changes || [],
  })));
  sync.runs.forEach((r) => rows.push({
    id: r.id, t: r.t, mod: '数据同步', ref: r.id, refTitle: r.src + ' 同步',
    act: r.result === 'ok' ? '同步完成' : r.result === 'drift' ? '同步完成（存在差异）' : '同步失败',
    who: r.by, dept: '系统', ok: r.result === 'ok',
    note: r.err || (r.upd ? '更新 ' + r.upd + ' 项' : '无字段变化'), changes: [],
  }));
  sync.diffs.filter((x) => x.status !== 'open').forEach((x) => rows.push({
    id: x.id, t: '2026-09-28 14:20', mod: '差异处理', ref: x.id, refTitle: x.mid + ' · ' + x.field,
    act: x.status === 'resolved' ? '采纳远端值' : '标记待复核', who: '供应部', dept: '供应部', ok: true,
    note: x.status === 'resolved' ? '已采用供应部最新报价' : '本地值超过 15 天未更新', changes: [],
  }));
  return rows.sort((a, b) => (a.t < b.t ? 1 : -1));
}

/* 角色可见范围 */
const ROLES = {
  finance: { key: 'finance', name: '财务部', user: '财务部', initials: '财', tint: '#2a5bdf' },
  prod: { key: 'prod', name: '生产车间', user: '生产车间', initials: '车', tint: '#17a2a2' },
  supply: { key: 'supply', name: '供应部', user: '供应部', initials: '供', tint: '#c2740a' },
};

/* 导航 */
const NAV = [
  { group: '总览' },
  { key: 'dashboard', icon: 'grid', label: '成本测算看板', roles: ['finance', 'prod', 'supply'] },
  { key: 'drugs', icon: 'layers', label: '药品管理', roles: ['finance', 'prod', 'supply'] },
  { group: '数据' },
  { key: 'sync', icon: 'plug', label: '主数据同步', roles: ['finance', 'supply'] },
  { key: 'materials', icon: 'box', label: '物料与价格', roles: ['finance', 'prod', 'supply'] },
  { key: 'rates', icon: 'sliders', label: '费用报工单价', roles: ['finance'] },
  { key: 'formula', icon: 'sigma', label: '公式口径', roles: ['finance'] },
  { group: '业务流程' },
  { key: 'tasks', icon: 'send', label: '测算任务下发', roles: ['finance'] },
  { key: 'declarations', icon: 'form', label: '成本测算单', roles: ['finance', 'prod', 'supply'] },
  { key: 'approvals', icon: 'check', label: '审批中心', roles: ['finance'] },
  { key: 'forecast', icon: 'calc', label: '成本测算结果', roles: ['finance', 'prod', 'supply'] },
  { group: '留痕' },
  { key: 'audit', icon: 'clock', label: '审计日志', roles: ['finance', 'prod', 'supply'] },
];

/* ------------------------------------------------------------------
   7. 应用状态
------------------------------------------------------------------ */
const AppCtx = createContext(null);
const useApp = () => useContext(AppCtx);

/* 后端地址。页面由后端托管时是同源空串；双击 html 打开时退回本地常量数据，不启用登录。 */
const API = null;
const TOKEN_KEY = 'cf-token';
const SERVICE_UNAVAILABLE = '无法连接系统服务，请确认系统已启动后重试';

function request(path, options) {
  return fetch(API + path, options).catch(() => {
    throw new Error(SERVICE_UNAVAILABLE);
  });
}

function api(method, path, body) {
  const headers = {};
  if (body) headers['Content-Type'] = 'application/json';
  const token = localStorage.getItem(TOKEN_KEY);
  if (token) headers['Authorization'] = 'Bearer ' + token;
  return request(path, {
    method: method,
    headers: headers,
    body: body ? JSON.stringify(body) : undefined,
  }).then((r) => {
    if (r.status === 401 && path !== '/api/login') {
      localStorage.removeItem(TOKEN_KEY);
      window.dispatchEvent(new Event('cf-logout'));
    }
    if (!r.ok) return r.json().catch(() => ({})).then((e) => {
      const detail = e && e.detail;
      const message = typeof detail === 'string' ? detail : detail && detail.message ? detail.message : (path + ' → ' + r.status);
      throw new Error(message);
    });
    return r.json();
  });
}

function apiFile(method, path, body, extraHeaders, responseType) {
  const headers = Object.assign({}, extraHeaders || {});
  const token = localStorage.getItem(TOKEN_KEY);
  if (token) headers.Authorization = 'Bearer ' + token;
  return request(path, { method, headers, body }).then((r) => {
    if (r.status === 401) {
      localStorage.removeItem(TOKEN_KEY);
      window.dispatchEvent(new Event('cf-logout'));
    }
    if (!r.ok) return r.json().catch(() => ({})).then((e) => {
      const detail = e && e.detail;
      const message = typeof detail === 'string' ? detail : detail && detail.message ? detail.message : (path + ' → ' + r.status);
      throw new Error(message);
    });
    if (responseType === 'blob') {
      return r.blob().then((data) => ({ data, headers: r.headers }));
    }
    return r.json();
  });
}

function buildCompleteness(modes, bom, factors) {
  const numeric = (v) => v !== null && v !== undefined && v !== '' && Number.isFinite(Number(v));
  const positive = (v) => numeric(v) && Number(v) > 0;
  const nonnegative = (v) => numeric(v) && Number(v) >= 0;
  const stage = (m, key) => (m.stages && m.stages[key]) || {};
  const stagePair = (m, test) => ['semi', 'fin'].every((key) => test(stage(m, key), key));
  const bomReady = bom.length > 0 && bom.every((line) =>
    positive(line.qty) && nonnegative(line.priceTax) && nonnegative(line.taxRate) && line.confirmed === true);
  const factorReady = factors.length > 0 && factors.every((line) =>
    nonnegative(line.wsPrice) && nonnegative(line.auxPrice) && nonnegative(line.taxRate));
  const out = {};
  modes.forEach((m) => {
    const staffRows = m.staff && Array.isArray(m.staff.rows) ? m.staff.rows : [];
    const staffReady = staffRows.length > 0
      && staffRows.some((row) => positive(row.n))
      && staffRows.every((row) => nonnegative(row.n) && nonnegative(row.hours) && nonnegative(row.total));
    out[m.key] = [
      {
        k: '批理论产量 / 成品率 / 批次', src: '生产车间',
        ok: positive(m.batches) && stagePair(m, (s) => positive(s.batchQty) && positive(s.yield) && Number(s.yield) <= 1),
      },
      {
        k: '产成品抽留样', src: '生产车间',
        ok: nonnegative(stage(m, 'fin').sample) && positive(stage(m, 'fin').sampleTimes),
      },
      {
        k: '生产时长 / 日总人工', src: '生产车间',
        ok: positive(m.hpd) && stagePair(m, (s) => positive(s.hours) && nonnegative(s.dailyLabor)),
      },
      {
        k: '单批次水电气汽', src: '生产车间',
        ok: stagePair(m, (s) => nonnegative(s.water) && nonnegative(s.elec) && nonnegative(s.steam)),
      },
      { k: '定员明细', src: '生产车间', ok: staffReady },
      { k: '物料含税单价确认', src: '供应部', ok: bomReady },
      { k: '报工单价', src: '财务部', ok: factorReady },
    ];
  });
  return out;
}

function AppProvider({ children }) {
  const [role, setRole] = useState('finance');
  const [user, setUser] = useState(null);
  const [authed, setAuthed] = useState(API === null);
  const [authReady, setAuthReady] = useState(API === null);
  const [route, setRoute] = useState('dashboard');
  const [nav, setNav] = useState({ route: 'dashboard', params: {} });
  const [decls, setDecls] = useState(DECLARATIONS_SEED);
  const [sync, setSync] = useState(SYNC_SEED);
  const [toasts, setToasts] = useState([]);
  const [roleHint, setRoleHint] = useState(null);
  /* 可编辑主数据：费用报工单价（KL01-KL10）与物料 BOM。后端在线时以库里的值为准。 */
  const [factors, setFactors] = useState(COST_FACTORS);
  const [bomLines, setBomLines] = useState(BOM_LINES);
  const [ready, setReady] = useState(!API && API !== '');
  /* 药品库：可切换、可新增、可导入。当前测算对象由 activeDrug 指向。 */
  const [drugs, setDrugs] = useState([{ id: PRODUCT.id, product: Object.assign({}, PRODUCT), modes: MODES, bom: BOM_LINES.map((b) => Object.assign({}, b)) }]);
  const [activeDrug, setActiveDrug] = useState(PRODUCT.id);
  const [activeMode, setActiveMode] = useState('M1');
  const [rev, setRev] = useState(0);
  const [workbooks, setWorkbooks] = useState([]);
  const [workbooksReady, setWorkbooksReady] = useState(API === null);
  const [workbooksError, setWorkbooksError] = useState('');
  const [editorService, setEditorService] = useState({ available: false, reason: '在线编辑服务未启用' });

  /* 启动时从后端拉全量状态，并替换公式引擎读的全局量。拉不到就用本地常量，页面照常可看。 */
  const reload = useCallback(() => {
    if (API === null) return Promise.resolve();
    return api('GET', '/api/state').then((s) => {
      Object.assign(PRODUCT, s.product);
      MODES.splice(0, MODES.length, ...s.modes);
      BOM_LINES.splice(0, BOM_LINES.length, ...s.bom);
      setFactors(s.factors);
      setBomLines(s.bom);
      setDecls(s.declarations);
      setSync(s.sync);
      setDrugs([{ id: s.product.id, product: Object.assign({}, s.product), modes: s.modes, bom: s.bom }]);
      setActiveDrug(s.product.id);
      setRev((n) => n + 1);
      setReady(true);
    }).catch(() => setReady(true));
  }, []);
  const reloadWorkbooks = useCallback(() => {
    if (API === null) return Promise.resolve();
    setWorkbooksReady(false);
    setWorkbooksError('');
    return api('GET', '/api/workbooks').then((result) => {
      setWorkbooks(result.items || []);
      setEditorService(result.editorService || { available: false, reason: '在线编辑服务未启用' });
      setWorkbooksReady(true);
      return result;
    }).catch((error) => {
      setWorkbooksError(error.message || '工作簿加载失败');
      setWorkbooksReady(true);
    });
  }, []);

  /* 有令牌就向后端确认身份；令牌失效（后端返回 401）时 api() 会派发 cf-logout。 */
  useEffect(() => {
    if (API === null) return;
    const off = () => { setUser(null); setAuthed(false); setAuthReady(true); setWorkbooks([]); };
    window.addEventListener('cf-logout', off);
    const token = localStorage.getItem(TOKEN_KEY);
    if (!token) {
      setAuthed(false);
      setAuthReady(true);
      return () => window.removeEventListener('cf-logout', off);
    }
    api('GET', '/api/me').then((u) => {
      setUser(u);
      setRole(u.role);
      setAuthed(true);
      setAuthReady(true);
    }).catch(() => {
      setAuthed(false);
      setAuthReady(true);
    });
    return () => window.removeEventListener('cf-logout', off);
  }, []);

  useEffect(() => {
    if (!authed || API === null) return;
    reload();
    reloadWorkbooks();
  }, [authed, reload, reloadWorkbooks]);

  const login = useCallback((username, password) => {
    return api('POST', '/api/login', { username: username, password: password }).then((res) => {
      localStorage.setItem(TOKEN_KEY, res.token);
      setUser(res.user);
      setRole(res.user.role);
      setAuthed(true);
      setAuthReady(true);
      setRoute('dashboard');
    });
  }, []);

  const logout = useCallback(() => {
    api('POST', '/api/logout', {}).catch(() => {});
    localStorage.removeItem(TOKEN_KEY);
    setUser(null);
    setAuthed(false);
    setWorkbooks([]);
  }, []);

  const toast = useCallback((msg, kind) => {
    const id = uid('toast');
    setToasts((ts) => ts.concat([{ id, msg, kind: kind || 'ok' }]));
    setTimeout(() => setToasts((ts) => ts.filter((t) => t.id !== id)), 3600);
  }, []);

  const go = useCallback((r, params) => {
    setRoute(r);
    setNav({ route: r, params: params || {} });
  }, []);

  /* 差异采纳后的单价覆盖 */
  const priceOverride = useMemo(() => {
    const o = {};
    sync.diffs.forEach((d) => {
      if (d.status === 'resolved' && d.field === '含税采购单价') o[d.mid] = { priceTax: d.remote };
    });
    return o;
  }, [sync.diffs]);

  /* 全 6 工况试算结果（随报工单价、物料清单、当前药品的切换实时重算） */
  const calc = useMemo(() => computeAll(priceOverride, bomLines, factors), [priceOverride, bomLines, factors, rev]);

  /* 数据完整度由实际字段计算；零值有效，空值与不适用分别处理。 */
  const COMPLETENESS = useMemo(
    () => buildCompleteness(MODES, bomLines, factors),
    [bomLines, factors, rev],
  );

  /* 待办 */
  const todo = useMemo(() => {
    const out = [];
    const activeDecls = decls.filter((d) => !d.product || d.product === activeDrug);
    if (role === 'finance') {
      activeDecls.filter((d) => d.step === 3 && d.status === 'reviewing').forEach((d) => out.push({ kind: 'approve', decl: d }));
      sync.diffs.filter((d) => d.status === 'open').forEach((d) => out.push({ kind: 'diff', diff: d }));
      const extendedModes = MODES.filter((m) => m.key === 'M5' || m.key === 'M6').map((m) => m.key);
      if (extendedModes.length && !activeDecls.some((d) => d.modes.some((m) => extendedModes.includes(m)) && d.step < 3 && d.status !== 'rejected')) {
        out.push({ kind: 'nudge', text: '当前药品的扩批工况数据未齐，无法出具正式测算' });
      }
    } else if (role === 'prod') {
      activeDecls.filter((d) => d.step === 1 && d.status !== 'approved').forEach((d) => out.push({ kind: 'fill', decl: d }));
    } else {
      activeDecls.filter((d) => d.step === 2 && d.status !== 'approved').forEach((d) => out.push({ kind: 'fill', decl: d }));
    }
    return out;
  }, [role, decls, sync.diffs, activeDrug, rev]);

  /* 动作：提交 / 审批（审核锁定）/ 驳回 / 回退。后端在线时先落库再回读，刷新不丢。 */
  const submitDeclaration = useCallback((id, data, note) => {
    const apply = (ds) => ds.map((d) => {
      if (d.id !== id) return d;
      const step = Math.min(d.step + 1, 4);
      const node = d.step;
      return {
        ...d,
        step,
        status: step === 3 ? 'reviewing' : d.status === 'rejected' ? 'pending' : d.status,
        payload: Object.assign({}, d.payload, data || {}),
        progress: data && data.progress ? Object.assign({}, d.progress, data.progress) : d.progress,
        timeline: d.timeline.concat([{
          t: NOW, node, act: STEPS[node].name + '提交',
          who: ROLES[role].user, dept: ROLES[role].name, ok: true, note: note || '',
          changes: (data && data.changes) || [],
        }, {
          t: NOW, node: step >= 4 ? 3 : step, act: '流转至' + STEPS[Math.min(step, 3)].name, who: '系统', dept: '—', ok: true, note: '', changes: [],
        }]),
      };
    });
    if (API === null) { setDecls(apply); return; }
    api('POST', '/api/declarations/' + id + '/submit',
        { note: note, changes: (data && data.changes) || [], payload: data || {} })
      .then(reload).catch((e) => toast('提交失败：' + e.message, 'warn'));
  }, [role, reload, toast]);

  const approveDeclaration = useCallback((id, note) => {
    const apply = (ds) => ds.map((d) => d.id === id ? {
      ...d, step: 4, status: 'approved', lockedAt: NOW, lockBy: ROLES.finance.user,
      timeline: d.timeline.concat([{ t: NOW, node: 3, act: '财务审批通过 · 审核锁定', who: ROLES.finance.user, dept: '财务部', ok: true, note: note || '数据齐备，纳入本期成本测算并锁定。', changes: [] }]),
    } : d);
    if (API === null) { setDecls(apply); return; }
    api('POST', '/api/declarations/' + id + '/approve', { note: note })
      .then(reload).catch((e) => toast('审批失败：' + e.message, 'warn'));
  }, [reload, toast]);

  const rejectDeclaration = useCallback((id, target, note, changedFields) => {
    const apply = (ds) => ds.map((d) => {
      if (d.id !== id) return d;
      const back = Math.min(target, d.step);
      return {
        ...d, step: Math.max(back, 1), status: 'rejected',
        timeline: d.timeline.concat([{
          t: NOW, node: 3, act: '财务驳回',
          who: ROLES.finance.user, dept: '财务部', ok: false,
          note: note, rejectTo: STEPS[back].name,
          changes: changedFields || [],
        }]),
      };
    });
    if (API === null) { setDecls(apply); return; }
    api('POST', '/api/declarations/' + id + '/reject', { note: note, target: target, changes: changedFields || [] })
      .then(reload).catch((e) => toast('驳回失败：' + e.message, 'warn'));
  }, [reload, toast]);

  const syncNow = useCallback((src) => {
    const apply = (s) => {
      const run = { id: uid('RUN'), t: NOW, src, result: src === 'PRICE' ? 'drift' : 'ok', add: 0, upd: src === 'PRICE' ? 2 : 0, del: 0, skip: src === 'PRICE' ? 43 : 60, dur: src === 'PRICE' ? '37s' : '9s', by: ROLES[role].user + '（手动）' };
      return { ...s, runs: [run].concat(s.runs), sources: s.sources.map((x) => x.id === src ? { ...x, last: NOW } : x) };
    };
    if (API === null) { setSync(apply); return; }
    api('POST', '/api/sync/' + src + '/run').then(reload)
      .catch((e) => toast('同步失败：' + e.message, 'warn'));
  }, [role, reload, toast]);

  const resolveDiff = useCallback((id, choice) => {
    const apply = (s) => ({
      ...s,
      diffs: s.diffs.map((d) => d.id === id ? { ...d, status: choice === 'accept' ? 'resolved' : 'stale', local: choice === 'accept' ? d.remote : d.local } : d),
    });
    if (API === null) { setSync(apply); return; }
    api('POST', '/api/sync/diffs/' + id, { choice: choice }).then(reload)
      .catch((e) => toast('差异处理失败：' + e.message, 'warn'));
  }, [reload, toast]);

  /* 编辑费用报工单价（财务录入）：code 定位费用项，patch 为要改的字段 */
  const updateFactor = useCallback((code, patch) => {
    setFactors((fs) => fs.map((f) => f.code === code ? Object.assign({}, f, patch) : f));
    if (API !== null) api('PATCH', '/api/factors/' + code, patch)
      .catch((e) => toast('报工单价未保存：' + e.message, 'warn'));
  }, [toast]);

  /* 编辑物料 BOM / 价格：id 定位物料，patch 为要改的字段（单耗、含税单价、税率、临测价等） */
  const updateBomLine = useCallback((id, patch) => {
    setBomLines((bs) => bs.map((b) => b.id === id ? Object.assign({}, b, patch) : b));
    setDrugs((ds) => ds.map((d) => d.id !== activeDrug ? d : Object.assign({}, d, {
      bom: d.bom.map((b) => b.id === id ? Object.assign({}, b, patch) : b),
    })));
    if (API !== null) api('PATCH', '/api/bom/' + id, patch)
      .catch((e) => toast('物料改动未保存：' + e.message, 'warn'));
  }, [activeDrug, toast]);

  /* 切换当前测算药品：替换全局产品 / 工况 / 物料并重算 */
  const switchDrug = useCallback((id) => {
    const drug = drugs.find((d) => d.id === id);
    if (!drug) return;
    applyDrug(drug);
    setBomLines(drug.bom);
    setActiveDrug(id);
    setActiveMode((mode) => drug.modes.some((m) => m.key === mode) ? mode : (drug.modes[0] && drug.modes[0].key) || 'M1');
    setRev((n) => n + 1);
  }, [drugs]);

  /* 新增药品：登记一个空白药品并切过去，车间数据与物料待后续填写或导入 */
  const addDrug = useCallback((info) => {
    const id = info.id || uid('XN');
    const product = {
      id: id, base: info.base || '', name: info.name || '未命名药品', spec: info.spec || '',
      cat: info.cat || '', packSpec: info.packSpec || '', packQty: Number(info.packQty) || 0,
      unit: info.unit || '盒', minUnit: info.minUnit || '片', minUnitFactor: Number(info.minUnitFactor) || 1,
      make: info.make || '自制', wsCode: info.wsCode || '', wsName: info.wsName || '', wsDesc: info.wsName || '',
    };
    const bom = (info.bom || []).map((b) => Object.assign({}, b));
    const modes = [blankMode('M1', '模式1')];
    const drug = { id: id, product: product, modes: modes, bom: bom };
    setDrugs((ds) => ds.concat([drug]));
    applyDrug(drug);
    setBomLines(bom);
    setActiveDrug(id);
    setActiveMode(modes[0].key);
    setRev((n) => n + 1);
    return id;
  }, []);

  /* 导入药品：传入已解析好的 { product, modes, bom }，登记并切换为当前测算对象 */
  const importDrug = useCallback((parsed) => {
    const id = (parsed.product && parsed.product.id) || uid('XN');
    const drug = { id: id, product: parsed.product, modes: parsed.modes || [], bom: parsed.bom || [] };
    setDrugs((ds) => ds.concat([drug]));
    applyDrug(drug);
    setBomLines(drug.bom);
    setActiveDrug(id);
    setActiveMode((drug.modes[0] && drug.modes[0].key) || 'M1');
    setRev((n) => n + 1);
    return id;
  }, []);

  const uploadWorkbook = useCallback((file) => {
    if (API === null) return Promise.reject(new Error('请通过系统服务地址上传工作簿'));
    if (!file || !/\.xlsx$/i.test(file.name)) return Promise.reject(new Error('请选择 .xlsx 文件'));
    return file.arrayBuffer().then((buffer) => apiFile(
      'POST', '/api/workbooks', buffer,
      {
        'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'X-File-Name': encodeURIComponent(file.name),
      },
    )).then((result) => reloadWorkbooks().then(() => result));
  }, [reloadWorkbooks]);

  const saveWorkbookVersion = useCallback((documentId, note) => {
    return api('POST', '/api/workbooks/' + documentId + '/versions', { note: note || '' })
      .then((result) => reloadWorkbooks().then(() => result));
  }, [reloadWorkbooks]);

  const downloadWorkbookVersion = useCallback((versionId, fileName) => {
    return apiFile('GET', '/api/workbook-versions/' + versionId + '/download', null, null, 'blob')
      .then(({ data }) => {
        const href = URL.createObjectURL(data);
        const link = document.createElement('a');
        link.href = href;
        link.download = fileName || '工作簿.xlsx';
        document.body.appendChild(link);
        link.click();
        link.remove();
        setTimeout(() => URL.revokeObjectURL(href), 1000);
      });
  }, []);

  const createWorkbookEditorSession = useCallback((documentId) => (
    api('POST', '/api/workbooks/' + documentId + '/editor-session', {})
  ), []);

  const heartbeatWorkbookEditor = useCallback((sessionId) => (
    api('POST', '/api/workbook-edit-sessions/' + sessionId + '/heartbeat', {})
  ), []);

  const closeWorkbookEditor = useCallback((sessionId) => {
    if (!sessionId) return Promise.resolve();
    return api('DELETE', '/api/workbook-edit-sessions/' + sessionId).catch(() => null)
      .then(() => reloadWorkbooks());
  }, [reloadWorkbooks]);

  const audit = useMemo(() => buildAuditLog(decls, sync), [decls, sync]);

  const value = {
    role, setRole, user, authed, authReady, login, logout,
    route, nav, go, decls, sync, toasts, toast,
    calc, COMPLETENESS, activeMode, setActiveMode, todo, audit, roleHint, setRoleHint, priceOverride,
    factors, bomLines, updateFactor, updateBomLine,
    drugs, activeDrug, switchDrug, addDrug, importDrug, rev,
    workbooks, workbooksReady, workbooksError, editorService, reloadWorkbooks,
    uploadWorkbook, saveWorkbookVersion, downloadWorkbookVersion,
    createWorkbookEditorSession, heartbeatWorkbookEditor, closeWorkbookEditor,
    submitDeclaration, approveDeclaration, rejectDeclaration, syncNow, resolveDiff,
    STEPS, STEP_FIELDS, PRODUCT, STAGES, MODES, BOM_LINES, COST_FACTORS, FIELDS, SRC_META, ROLES, NOW, TODAY, NAV,
  };
  return React.createElement(AppCtx.Provider, { value }, children);
}

Object.assign(window, {
  AppCtx, useApp, AppProvider, api, apiFile, buildCompleteness,
  PRODUCT, STAGES, MODES, modeById, BOM_LINES, bomLineById, COST_FACTORS,
  FORMULAS, FIELDS, SRC_META, notaxPrice, applyDrug, blankMode,
  computeStageStd, computeMaterialCost, computeFactorCost, computeMode, computeAll, modeDefaults,
  STEPS, STEP_FIELDS, DECLARATIONS_SEED, SYNC_SEED, buildAuditLog,
  ROLES, NAV, NOW, TODAY, uid,
});
