// field-test.jsx  v2 — 现场测试批量录入
// P0：外壳 + 本地数据层自检。
// P1.2：测试计划管理（列表 / 新建：选测试项 + 选运动员 + 日期 / 选中工作区）。
// 全局作用域（无模块化）：用 React.* 以避免与其它文件的全局 Hook 声明冲突；
// 组件名加 Field 前缀以防全局命名碰撞。

function FieldTestStoreStatus({ ready }) {
  const m = {
    'null':  { c: 'var(--muted)',        t: '检查本地数据库…' },
    'true':  { c: 'var(--pos, #16a34a)', t: '本地数据库就绪 ✓' },
    'false': { c: 'var(--neg, #dc2626)', t: '本地数据库不可用 ✕' },
  }[String(ready)];
  return <div style={{ fontSize: 12, color: m.c, fontWeight: 500, whiteSpace: 'nowrap' }}>{m.t}</div>;
}

function FieldAutosaveStatus({ state = 'idle', lastSavedAt = null }) {
  const copy = {
    idle: { label: '本地即存', color: 'var(--muted)', bg: 'rgba(15,23,42,.045)' },
    pending: { label: '待保存', color: 'var(--warn, #d97706)', bg: 'rgba(217,119,6,.09)' },
    saving: { label: '正在保存…', color: 'var(--accent, #2563eb)', bg: 'rgba(37,99,235,.08)' },
    saved: { label: '已保存', color: 'var(--pos, #16a34a)', bg: 'rgba(22,163,74,.08)' },
    error: { label: '保存失败', color: 'var(--neg, #dc2626)', bg: 'rgba(220,38,38,.08)' },
  }[state] || { label: '本地即存', color: 'var(--muted)', bg: 'rgba(15,23,42,.045)' };
  const time = lastSavedAt ? new Date(lastSavedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '尚无保存时间';
  return (
    <div data-field-autosave-status="true" style={{
      display: 'inline-flex',
      alignItems: 'center',
      gap: 8,
      padding: '5px 9px',
      borderRadius: 999,
      border: '1px solid rgba(15,23,42,.08)',
      background: copy.bg,
      color: copy.color,
      fontSize: 11.5,
      fontWeight: 650,
      lineHeight: 1.2,
      whiteSpace: 'nowrap',
    }}>
      <span data-field-autosave-state="true">{copy.label}</span>
      <span style={{ color: 'var(--muted)', fontWeight: 500 }}>Last {time}</span>
    </div>
  );
}

// 可切换的选择「药丸」按钮（测试项 / 运动员通用）
function FieldChip({ active, onClick, children }) {
  return (
    <button type="button" onClick={onClick} style={{
      padding: '7px 13px', borderRadius: 999, cursor: 'pointer',
      border: '1px solid ' + (active ? 'var(--accent, #2563eb)' : 'var(--border)'),
      background: active ? 'var(--accent, #2563eb)' : 'var(--panel, #fff)',
      color: active ? '#fff' : 'var(--text-2)',
      fontSize: 13, fontFamily: 'inherit', minHeight: 36, transition: 'all .12s',
    }}>{children}</button>
  );
}

// 统一解析一个测试项：先查自定义(FieldDataStore.testDefs)，再查内置指标目录。
function fieldResolveTest(id, groups, customTests) {
  const c = (customTests || []).find(t => t.id === id);
  if (c) return c;
  for (const g of (groups || [])) { const m = (g.metrics || []).find(x => x.id === id); if (m) return m; }
  return null;
}

// 临场新建测试项的小表单：名称 + 单位 + 方向。
function FieldNewTestForm({ onCreate }) {
  const [open, setOpen] = React.useState(false);
  const [label, setLabel] = React.useState('');
  const [unit, setUnit] = React.useState('');
  const [dir, setDir] = React.useState('lower');
  const [trialsN, setTrialsN] = React.useState('1');
  const inputStyle = { padding: '7px 9px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--panel, #fff)', color: 'var(--text)', font: 'inherit', fontSize: 13 };
  if (!open) return <button type="button" className="btn ghost" style={{ fontSize: 13 }} onClick={() => setOpen(true)}>+ 添加临场测试项</button>;
  const submit = async () => {
    if (!label.trim()) return;
    await onCreate({ label: label.trim(), unit: unit.trim(), dir, defaultTrials: Math.max(1, Number(trialsN) || 1) });
    setLabel(''); setUnit(''); setDir('lower'); setTrialsN('1'); setOpen(false);
  };
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', padding: '8px 10px', border: '1px dashed var(--border)', borderRadius: 8, width: '100%', boxSizing: 'border-box' }}>
      <input value={label} onChange={e => setLabel(e.target.value)} placeholder="项目名（如 505 左）" style={{ ...inputStyle, flex: '1 1 150px' }} />
      <input value={unit} onChange={e => setUnit(e.target.value)} placeholder="单位 s/cm…" style={{ ...inputStyle, width: 86 }} />
      <input value={trialsN} onChange={e => setTrialsN(e.target.value)} title="默认试做次数" placeholder="次数" inputMode="numeric" style={{ ...inputStyle, width: 64 }} />
      <div style={{ display: 'flex', gap: 4 }}>
        {[['lower', '越低越好'], ['higher', '越高越好'], ['neutral', '中性']].map(([v, t]) => (
          <button key={v} type="button" onClick={() => setDir(v)} style={{ padding: '6px 9px', borderRadius: 6, fontSize: 12, cursor: 'pointer', border: '1px solid ' + (dir === v ? 'var(--accent, #2563eb)' : 'var(--border)'), background: dir === v ? 'var(--accent, #2563eb)' : 'transparent', color: dir === v ? '#fff' : 'var(--text-2)' }}>{t}</button>
        ))}
      </div>
      <button type="button" className="btn primary" style={{ fontSize: 13, opacity: label.trim() ? 1 : 0.5 }} onClick={submit} disabled={!label.trim()}>添加</button>
      <button type="button" className="btn ghost" style={{ fontSize: 13 }} onClick={() => setOpen(false)}>取消</button>
    </div>
  );
}

function FieldMetricIntakeNote() {
  return (
    <div style={{
      margin: '0 0 10px',
      padding: '9px 11px',
      border: '1px solid var(--border)',
      borderRadius: 8,
      background: 'var(--panel, #fff)',
      color: 'var(--text-2)',
      fontSize: 12,
      lineHeight: 1.55,
    }}>
      <strong style={{ color: 'var(--text)' }}>临场测试项。</strong>
      这里用于补充本次 Field Test 的采集来源，不会自动成为长期指标定义。
      需要长期追踪的指标，仍应在 Settings / Metrics 统一治理。
    </div>
  );
}

function FieldPromotionPathNote() {
  return (
    <div data-field-promotion-path-note="true" style={{
      margin: '8px 0 0',
      padding: '8px 10px',
      border: '1px solid rgba(37, 99, 235, .18)',
      borderRadius: 8,
      background: 'rgba(37, 99, 235, .055)',
      color: 'var(--text-2)',
      fontSize: 12,
      lineHeight: 1.5,
    }}>
      <strong style={{ color: 'var(--text)' }}>正式指标路径。</strong>
      点击「提升为正式指标」会进入现有确认流程，只复制定义到 Settings / Metrics；不会迁移或改写已有 Field Test 录入记录。
    </div>
  );
}

// 新建测试计划：名称 + 日期 + 勾测试项（含自定义） + 勾运动员
function FieldPlanBuilder({ groups, athletes, customTests = [], initialDraft = null, onCreateTest, onPromoteTest, onSave, onCancel }) {
  const today = new Date().toISOString().slice(0, 10);
  const [name, setName] = React.useState(initialDraft?.name || '');
  const [date, setDate] = React.useState(initialDraft?.date || today);
  const [tests, setTests] = React.useState(initialDraft?.testDefIds || []);    // metric ids
  const [people, setPeople] = React.useState(initialDraft?.athleteIds || []);  // athlete ids
  const [saving, setSaving] = React.useState(false);

  // 函数式更新：避免同一渲染批次内多次切换读到过期状态而互相覆盖。
  const toggle = (setArr, id) => setArr(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
  const canSave = !!name.trim() && tests.length > 0 && people.length > 0 && !saving;

  const inputStyle = { padding: '8px 10px', borderRadius: 6, border: '1px solid var(--border)', background: 'var(--panel, #fff)', color: 'var(--text)', font: 'inherit', fontSize: 14 };
  const sectionTitle = { fontSize: 13, fontWeight: 600, color: 'var(--text)', margin: '18px 0 8px', display: 'flex', alignItems: 'center', gap: 10 };

  const submit = async () => {
    if (!canSave) return;
    setSaving(true);
    try {
      await onSave({ name: name.trim(), date, testDefIds: [...tests], athleteIds: [...people] });
    } finally { setSaving(false); }
  };

  return (
    <div className="field-plan-builder-shell">
      <aside className="field-plan-builder-steps" aria-label="计划创建步骤">
        {[['1', '基本信息', '名称与日期'], ['2', '测试项目', `${tests.length} 项已选`], ['3', '运动员名单', `${people.length} 人已选`], ['4', '确认并创建', '进入现场录入']].map(([n, label, note], index) => (
          <div key={n} className={index === 0 ? 'on' : ''}><i>{n}</i><span><b>{label}</b><small>{note}</small></span></div>
        ))}
      </aside>
      <section className="field-plan-builder-form">
      <header className="field-plan-builder-head"><div><h3>创建测试计划</h3><p>模板只复制测试项目和名单，不复制历史结果。</p></div></header>
      {initialDraft?.sourceTemplateName ? (
        <div style={{ marginBottom: 14, padding: '9px 11px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--panel, #fff)', color: 'var(--text-2)', fontSize: 12, lineHeight: 1.55 }}>
          <strong style={{ color: 'var(--text)' }}>从模板创建。</strong>
          已载入「{initialDraft.sourceTemplateName}」的测试项与运动员，请确认日期、名单和项目后再创建新计划。不会复制历史结果。
        </div>
      ) : null}

      <div style={{ display: 'flex', gap: 14, flexWrap: 'wrap' }}>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: 'var(--muted)', flex: '1 1 240px' }}>
          计划名称
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder="如：季前体能测试" style={inputStyle} />
        </label>
        <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 12, color: 'var(--muted)', flex: '0 0 180px' }}>
          测试日期
          <input type="date" value={date} onChange={(e) => setDate(e.target.value)} style={inputStyle} />
        </label>
      </div>

      <div style={sectionTitle}>选择测试项 <span style={{ color: 'var(--muted)', fontWeight: 400 }}>({tests.length})</span></div>
      {groups.map(g => {
        const metrics = (g.metrics || []).filter(m => !m.computed);
        if (!metrics.length) return null;
        return (
          <div key={g.id} style={{ marginBottom: 10 }}>
            <div style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: '.06em', color: g.accent || 'var(--muted)', marginBottom: 6 }}>{g.label}</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              {metrics.map(m => (
                <FieldChip key={m.id} active={tests.includes(m.id)} onClick={() => toggle(setTests, m.id)}>
                  {m.label}{m.unit ? ` (${m.unit})` : ''}
                </FieldChip>
              ))}
            </div>
          </div>
        );
      })}

      <div style={sectionTitle}>临场测试项 <span style={{ color: 'var(--muted)', fontWeight: 400 }}>({customTests.length})</span></div>
      <FieldMetricIntakeNote />
      {customTests.length > 0 ? <FieldPromotionPathNote /> : null}
      <div data-field-custom-test-promotion-list="true" style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'stretch', marginTop: customTests.length > 0 ? 10 : 0 }}>
        {customTests.map(m => (
          <div key={m.id} data-field-custom-test-promotion-row="true" data-field-promotion-manual-only="true" style={{
            display: 'grid',
            gridTemplateColumns: 'minmax(0, 1fr) auto',
            alignItems: 'center',
            gap: 8,
            padding: '8px 9px',
            border: '1px solid var(--border)',
            borderRadius: 8,
            background: 'var(--panel, #fff)',
          }}>
            <div style={{ minWidth: 0 }}>
              <FieldChip active={tests.includes(m.id)} onClick={() => toggle(setTests, m.id)}>
                {m.label}{m.unit ? ` (${m.unit})` : ''}
              </FieldChip>
              <div style={{ marginTop: 5, fontSize: 11, color: 'var(--muted)', whiteSpace: 'nowrap' }}>
                {m.dir === 'lower' ? '越低越好' : m.dir === 'higher' ? '越高越好' : '中性'} · 手动提升 · 不改历史记录
              </div>
            </div>
            {onPromoteTest ? (
              <button type="button" className="btn ghost" style={{ fontSize: 12, padding: '6px 9px' }} onClick={() => onPromoteTest(m.id)} title="复制为 Settings / Metrics 正式指标，不改写已有录入记录">
                提升为正式指标
              </button>
            ) : null}
          </div>
        ))}
        <FieldNewTestForm onCreate={async (def) => { const c = await onCreateTest(def); if (c) setTests(prev => prev.includes(c.id) ? prev : [...prev, c.id]); }} />
      </div>

      <div style={sectionTitle}>
        选择运动员 <span style={{ color: 'var(--muted)', fontWeight: 400 }}>({people.length})</span>
        <button type="button" className="btn ghost" style={{ marginLeft: 'auto', fontSize: 12 }}
          onClick={() => setPeople(people.length === athletes.length ? [] : athletes.map(a => a.id))}>
          {people.length === athletes.length ? '清空' : '全选'}
        </button>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
        {athletes.map(a => (
          <FieldChip key={a.id} active={people.includes(a.id)} onClick={() => toggle(setPeople, a.id)}>
            {a.name}
          </FieldChip>
        ))}
      </div>
      </section>
      <aside className="field-plan-builder-summary">
        <h3>计划摘要</h3>
        <dl>
          <div><dt>测试日期</dt><dd>{date || '未选择'}</dd></div>
          <div><dt>测试项目</dt><dd>{tests.length} 项</dd></div>
          <div><dt>运动员</dt><dd>{people.length} 人</dd></div>
          <div><dt>默认录入方式</dt><dd>按运动员工位可随时切换</dd></div>
        </dl>
        <div className="field-plan-builder-actions">
        <button type="button" className="btn ghost" onClick={onCancel}>取消</button>
        <button type="button" className="btn primary" disabled={!canSave} onClick={submit} style={{ opacity: canSave ? 1 : 0.5 }}>
          {saving ? '创建中…' : '创建并开始'}
        </button>
        </div>
      </aside>
      </div>
  );
}

function FieldPlanProgressBadge({ progress }) {
  const entered = (progress && Number(progress.entered)) || 0;
  const total = (progress && Number(progress.total)) || 0;
  const done = total > 0 && entered >= total;
  const started = entered > 0;
  const label = done ? '已完成' : started ? '进行中' : '未开始';
  const color = done ? 'var(--pos, #16a34a)' : started ? 'var(--accent, #2563eb)' : 'var(--muted)';
  return (
    <span style={{
      flexShrink: 0,
      padding: '3px 8px',
      borderRadius: 999,
      border: '1px solid var(--border)',
      color,
      fontSize: 11,
      fontWeight: 600,
      background: done ? 'rgba(22,163,74,.07)' : started ? 'rgba(37,99,235,.07)' : 'var(--panel-2, #f7f6f2)',
      whiteSpace: 'nowrap',
    }}>{label}</span>
  );
}

function FieldPlanProgressBar({ progress }) {
  const entered = (progress && Number(progress.entered)) || 0;
  const total = (progress && Number(progress.total)) || 0;
  const pct = total > 0 ? Math.max(0, Math.min(100, Math.round((entered / total) * 100))) : 0;
  const done = total > 0 && entered >= total;
  const started = entered > 0;
  return (
    <div data-field-plan-progress-bar="true" style={{ marginTop: 8 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 4, fontSize: 11, color: 'var(--muted)' }}>
        <span>{total > 0 ? `${entered}/${total} 人已录` : '暂无运动员'}</span>
        <span>{pct}%</span>
      </div>
      <div style={{ height: 5, borderRadius: 999, background: 'rgba(15,23,42,.07)', overflow: 'hidden' }}>
        <div style={{
          width: pct + '%',
          height: '100%',
          borderRadius: 999,
          background: done ? 'var(--pos, #16a34a)' : started ? 'var(--accent, #2563eb)' : 'rgba(15,23,42,.18)',
          transition: 'width .18s ease',
        }} />
      </div>
    </div>
  );
}

// 计划列表
function FieldPlanList({ plans, templates = [], progress = {}, groups = [], athletes = [], customTests = [], onOpen, onNew, onNewFromTemplate, onSaveTemplate, onRenameTemplate, onDeleteTemplate, onDelete }) {
  const describeTemplateTests = (template) => {
    const labels = (template.testDefIds || []).map(id => {
      const test = fieldResolveTest(id, groups, customTests);
      return test ? test.label : id;
    }).filter(Boolean);
    return labels.length ? labels.join('、') : '未选择测试项';
  };
  const describeTemplateAthletes = (template) => {
    const labels = (template.athleteIds || []).map(id => {
      const athlete = athletes.find(a => a.id === id);
      return athlete ? athlete.name : id;
    }).filter(Boolean);
    return labels.length ? labels.join('、') : '未选择运动员';
  };
  const today = new Date().toISOString().slice(0, 10);
  const totalAthletes = plans.reduce((sum, plan) => sum + ((progress[plan.id] && Number(progress[plan.id].total)) || (plan.athleteIds || []).length), 0);
  const enteredAthletes = plans.reduce((sum, plan) => sum + ((progress[plan.id] && Number(progress[plan.id].entered)) || 0), 0);
  const outstandingAthletes = Math.max(0, totalAthletes - enteredAthletes);
  const completionPct = totalAthletes ? Math.round((enteredAthletes / totalAthletes) * 100) : 0;
  const nextPlan = plans.find(plan => {
    const p = progress[plan.id] || {};
    return Number(p.entered || 0) < Number(p.total || (plan.athleteIds || []).length);
  }) || plans[0] || null;
  return (
    <div className="field-plan-queue" data-field-plan-queue>
      <section className="field-queue-kpis" data-field-queue-kpis>
        <article><span>今日计划</span><strong>{plans.filter(plan => plan.date === today).length}</strong><small>共 {plans.length} 个可执行计划</small></article>
        <article><span>待完成运动员</span><strong>{outstandingAthletes}</strong><small>已录入 {enteredAthletes} / {totalAthletes}</small></article>
        <article><span>完成率</span><strong className="positive">{completionPct}%</strong><small>基于计划内运动员记录</small></article>
        <article><span>复用资产</span><strong>{templates.length + customTests.length}</strong><small>{templates.length} 模板 · {customTests.length} 临场项</small></article>
      </section>
      <div className="field-queue-grid">
        <section className="field-queue-panel">
          <header className="field-queue-head">
            <div><h3>测试计划队列</h3><p>按执行状态打开计划；模板与现场任务分开管理。</p></div>
            <button type="button" className="btn primary" onClick={onNew}>＋ 新建计划</button>
          </header>
          {plans.length === 0 ? (
            <div className="field-queue-empty">还没有测试计划。创建计划后，可按运动员、工位或分步模式录入。</div>
          ) : (
            <div className="field-plan-stack">
              {plans.map(p => (
                <article key={p.id} data-field-plan-queue-card="true" className="field-plan-row">
                  <div className="field-plan-date"><strong>{String(p.date || '').slice(8, 10) || '—'}</strong><small>{String(p.date || '').slice(5, 7) || '—'}月</small></div>
                  <button type="button" className="field-plan-copy" onClick={() => onOpen(p.id)}>
                    <span><b>{p.name || '未命名计划'}</b><FieldPlanProgressBadge progress={progress[p.id]} /></span>
                    <small>{p.date} · {(p.testDefIds || []).length} 测试项 · {(p.athleteIds || []).length} 人</small>
                    <FieldPlanProgressBar progress={progress[p.id]} />
                  </button>
                  <div className="field-plan-actions">
                    <button type="button" className="btn primary" onClick={() => onOpen(p.id)}>打开录入</button>
                    <details>
                      <summary aria-label="计划更多操作">•••</summary>
                      <div><button type="button" onClick={() => onSaveTemplate(p)}>存为模板</button><button type="button" className="danger" onClick={() => onDelete(p.id)}>删除计划</button></div>
                    </details>
                  </div>
                </article>
              ))}
            </div>
          )}
          <details data-field-template-management="true" className="field-template-manager">
            <summary>模板与临场测试项管理 <span>{templates.length} 模板 · {customTests.length} 临场项</span></summary>
            <p>模板只复用测试项目与运动员名单，新计划为空，不复制历史结果。</p>
            <div className="field-template-list">
              {templates.length ? templates.map(t => (
                <article key={t.id} data-field-template-empty-plan-note="true">
                  <div><b>{t.name || '未命名模板'}</b><small title={describeTemplateTests(t)}>{(t.testDefIds || []).length} 项 · {(t.athleteIds || []).length} 人 · {describeTemplateAthletes(t)}</small></div>
                  <button type="button" className="btn ghost" onClick={() => onNewFromTemplate(t.id)}>新建空计划</button>
                  <button type="button" className="btn ghost" onClick={() => onRenameTemplate(t.id)}>重命名</button>
                  <button type="button" className="btn ghost danger" onClick={() => onDeleteTemplate(t.id)}>删除</button>
                </article>
              )) : <div className="field-queue-empty">尚无模板。可从任一测试计划另存模板。</div>}
            </div>
          </details>
        </section>
        <aside className="field-next-action" data-field-next-action>
          <span className="field-next-kicker">NEXT ACTION / 下一步</span>
          {nextPlan ? (
            <React.Fragment>
              <h3>{nextPlan.name || '未命名计划'}</h3>
              <p>{nextPlan.date} · {(nextPlan.testDefIds || []).length} 项 · {(nextPlan.athleteIds || []).length} 人</p>
              <FieldPlanProgressBar progress={progress[nextPlan.id]} />
              <button type="button" className="btn primary" onClick={() => onOpen(nextPlan.id)}>进入当前计划</button>
            </React.Fragment>
          ) : (
            <React.Fragment><h3>创建第一份测试计划</h3><p>先确定测试项目、名单和日期，再进入现场录入。</p><button type="button" className="btn primary" onClick={onNew}>新建计划</button></React.Fragment>
          )}
          <FieldEvidencePathNote />
        </aside>
      </div>
    </div>
  );
}

function FieldEvidencePathNote() {
  return (
    <div style={{
      marginBottom: 14,
      padding: '10px 12px',
      border: '1px solid var(--border)',
      borderRadius: 8,
      background: 'var(--panel, #fff)',
      color: 'var(--text-2)',
      fontSize: 12,
      lineHeight: 1.55,
    }}>
      <strong style={{ color: 'var(--text)' }}>采集边界。</strong>
      这里负责采集 observations，不直接生成审核结论。录入完成后数据会回写到既有看板；
      只有经过运动科学家审核的来源，才会进入 Reviewed Sessions。临场测试项可在这里创建；
      长期指标定义仍由 Settings / Metrics 治理。
    </div>
  );
}

function FieldTestContextBar({ mode, activePlan, entryMode, dbReady, onQueue, onBuilder, onEntryMode }) {
  return (
    <section className="field-workflow-hero">
      <div className="field-workflow-hero-copy">
        <div className="field-workflow-kicker">LAB / FIELD TEST</div>
        <h2>{mode === 'entry' && activePlan ? activePlan.name : '把现场测试变成可执行队列'}</h2>
        <p>{mode === 'entry'
          ? '当前计划中的数值会自动保存；明确确认后才写入运动员档案并进入后续审核。'
          : '计划先定义名单和测试项；现场只处理下一位、下一项与异常状态。原始记录保存后仍需审核。'}</p>
      </div>
      <div className="field-workflow-status"><FieldTestStoreStatus ready={dbReady}/><span>{activePlan ? `${(activePlan.testDefIds || []).length} 项 · ${(activePlan.athleteIds || []).length} 人` : '计划驱动 · 离线优先'}</span></div>
      <nav className="field-workflow-tabs" data-field-workflow-tabs aria-label="测试录入工作流">
        <button type="button" className={mode === 'queue' ? 'on' : ''} onClick={onQueue}>计划队列</button>
        <button type="button" className={mode === 'builder' ? 'on' : ''} onClick={onBuilder}>新建计划</button>
        <button type="button" className={mode === 'entry' && entryMode !== 'stepper' ? 'on' : ''} onClick={() => onEntryMode('station')}>现场录入</button>
        <button type="button" className={mode === 'entry' && entryMode === 'stepper' ? 'on' : ''} onClick={() => onEntryMode('stepper')}>移动分步</button>
      </nav>
    </section>
  );
}

function FieldWorkspaceFrame({ children, mode }) {
  return (
    <section data-field-workspace-frame="true" className={`field-workspace-frame field-workspace-${mode}`}>
      <div data-field-workspace-main="true">{children}</div>
    </section>
  );
}

// 单个测试项卡片：多试做 + 自动最优(高亮) + 历史参考 + 异常提醒。
function FieldTestCard({ test, slice, history, norm, onTrial, onAddTrial, onCommit, focused = false }) {
  const FS = window.FieldDataStore;
  const trials = (() => { const dt = Math.max(1, Number(test.defaultTrials) || 1); const have = (slice && slice.trials) || []; return have.length >= dt ? have : have.concat(Array.from({ length: dt - have.length }, () => ({ value: '' }))); })();
  const cb = FS.computeBest(trials, test.dir);
  const dirText = test.dir === 'lower' ? '越低越好' : test.dir === 'higher' ? '越高越好' : '中性';
  // 异常提醒：仅对带常模区间(worst/best)的内置指标启用；自定义项无区间则跳过。
  const lo = (test.worst != null && test.best != null) ? Math.min(test.worst, test.best) : null;
  const hi = (test.worst != null && test.best != null) ? Math.max(test.worst, test.best) : null;
  const margin = lo != null ? ((hi - lo) * 0.6 || 1) : null;
  const isOutlier = (v) => lo != null && v !== '' && v != null && isFinite(+v) && (+v < lo - margin || +v > hi + margin);
  return (
    <div data-field-source-test-focus={focused ? 'true' : undefined} style={{
      padding: focused ? '14px 12px' : '14px 0',
      borderBottom: '1px solid var(--border)',
      borderRadius: focused ? 8 : 0,
      background: focused ? 'rgba(37, 99, 235, .06)' : 'transparent',
      outline: focused ? '1px solid rgba(37, 99, 235, .22)' : 'none',
      outlineOffset: focused ? -1 : 0,
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 8, gap: 10 }}>
        <div style={{ minWidth: 0 }}>
          <span style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>{test.label}</span>
          <span style={{ fontSize: 11, color: 'var(--muted)', marginLeft: 8 }}>{test.unit || '—'} · {dirText}</span>
        </div>
        <div style={{ fontSize: 13, fontWeight: 600, whiteSpace: 'nowrap', color: cb.best != null ? 'var(--pos, #16a34a)' : 'var(--muted)' }}>
          {cb.best != null ? `最佳 ${cb.best}${test.unit || ''}` : '—'}
        </div>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
        {trials.map((t, i) => {
          const isBest = cb.best != null && cb.bestTrialIdx === i;
          const out = isOutlier(t && t.value);
          const bc = out ? 'var(--warn, #d97706)' : (isBest ? 'var(--pos, #16a34a)' : 'var(--border)');
          return (
            <input
              key={i} type="text" inputMode="decimal"
              value={t && t.value != null ? t.value : ''}
              onChange={(e) => onTrial(test.id, i, e.target.value)}
              onBlur={onCommit}
              placeholder={`第${i + 1}次`}
              title={out ? '数值可能异常，请核对' : ''}
              style={{
                width: 88, padding: '9px 10px', borderRadius: 6, textAlign: 'right', font: 'inherit', fontSize: 15,
                border: '1px solid ' + bc,
                background: out ? 'rgba(217,119,6,.07)' : (isBest ? 'rgba(22,163,74,.07)' : 'var(--panel, #fff)'), color: 'var(--text)',
              }}
            />
          );
        })}
        <button type="button" onClick={() => onAddTrial(test.id)} title="加一次试做"
          style={{ width: 38, height: 38, borderRadius: 6, border: '1px dashed var(--border)', background: 'transparent', color: 'var(--muted)', cursor: 'pointer', fontSize: 20, lineHeight: 1, flexShrink: 0 }}>+</button>
      </div>
      {history && (history.last != null || history.best != null) ? (
        <div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 6 }}>
          历史{history.last != null ? ` · 上次 ${history.last}${test.unit || ''}` : ''}{history.best != null ? ` · 最佳 ${history.best}${test.unit || ''}` : ''}
        </div>
      ) : null}
      {(() => { const nc = norm && cb.best != null ? FS.normCompare(norm, cb.best, test.dir) : null; return nc ? (
        <div style={{ fontSize: 11, color: 'var(--accent, #2563eb)', marginTop: 4 }}>
          对比常模{nc.percentile != null ? ` · 第 ${nc.percentile} 百分位` : ''}{nc.band ? ` · ${nc.band}` : ''}{nc.z != null ? ` · z ${nc.z > 0 ? '+' : ''}${nc.z}` : ''}
        </div>
      ) : null; })()}
    </div>
  );
}

// 选中计划后的工作区：左名单 / 右逐人录入（P1.4：多试做 + 自动最优 + 去抖自动保存）
// 窄屏(手机)检测：用于把两栏布局改为上下堆叠。
function useFieldNarrow(breakpoint = 700) {
  const [narrow, setNarrow] = React.useState(() => typeof window !== 'undefined' && window.innerWidth <= breakpoint);
  React.useEffect(() => {
    const mq = window.matchMedia('(max-width: ' + breakpoint + 'px)');
    const on = () => setNarrow(mq.matches);
    on();
    if (mq.addEventListener) mq.addEventListener('change', on); else mq.addListener(on);
    return () => { if (mq.removeEventListener) mq.removeEventListener('change', on); else mq.removeListener(on); };
  }, [breakpoint]);
  return narrow;
}

// 单运动员记录的编辑逻辑(供站点行复用)：编辑只保存草稿；明确确认后才发布到档案。
function useFieldRecord(planId, athleteId, date, planTests, patchAthleteMetrics, onSaved, onSaveState) {
  const FS = window.FieldDataStore;
  const Intake = window.NativeFieldIntake;
  const [record, setRecord] = React.useState(null);
  const recordRef = React.useRef(null);
  React.useEffect(() => { recordRef.current = record; }, [record]);
  const saveTimer = React.useRef(null);
  const mountedRef = React.useRef(true);
  const persistRef = React.useRef(null);
  const recompute = (cur) => {
    const results = { ...cur.results };
    planTests.forEach(t => { const r = results[t.id]; if (r && r.trials) { const cb = FS.computeBest(r.trials, t.dir); results[t.id] = { ...r, unit: t.unit, best: cb.best, bestTrialIdx: cb.bestTrialIdx }; } });
    return { ...cur, results };
  };
  const persist = async () => {
    const cur = recordRef.current; if (!cur) return;
    const next = recompute(cur); recordRef.current = next;
    if (mountedRef.current && onSaveState) onSaveState('saving');
    try {
      await FS.saveRecord(next);
      if (mountedRef.current && onSaved) onSaved();
      if (mountedRef.current && onSaveState) onSaveState('saved');
      return next;
    } catch (e) {
      if (mountedRef.current && onSaveState) onSaveState('error');
      throw e;
    }
  };
  persistRef.current = persist;
  const scheduleSave = () => { if (mountedRef.current && onSaveState) onSaveState('pending'); if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => { saveTimer.current = null; persistRef.current(); }, 500); };
  const flush = () => { if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; } persistRef.current(); };
  const confirm = async () => {
    if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
    const cur = recordRef.current; if (!cur) return false;
    const next = recompute(cur);
    if (mountedRef.current && onSaveState) onSaveState('saving');
    try {
      const retry = next.meta?.nativeIntake?.state === 'confirmed' && next.meta.nativeIntake.publishState === 'pending';
      const confirmation = retry
        ? { record: next, ...Intake.buildPublish(next) }
        : Intake.buildConfirmation({ record: next, tests: planTests, confirmedAt: new Date().toISOString() });
      if (!retry) await FS.confirmRecord(confirmation.record);
      recordRef.current = confirmation.record;
      if (mountedRef.current) setRecord(confirmation.record);
      if (patchAthleteMetrics) patchAthleteMetrics(confirmation.record.athleteId, confirmation.record.date, confirmation.patch, confirmation.sourceMeta);
      const published = Intake.markPublished(confirmation.record, new Date().toISOString());
      await FS.saveRecord(published);
      recordRef.current = published;
      if (mountedRef.current) setRecord(published);
      if (mountedRef.current && onSaved) onSaved();
      if (mountedRef.current && onSaveState) onSaveState('saved');
      return true;
    } catch (e) {
      if (mountedRef.current && onSaveState) onSaveState('error');
      return false;
    }
  };
  React.useEffect(() => {
    let alive = true;
    if (!athleteId) { setRecord(null); return; }
    (async () => { const r = await FS.ensureRecord(planId, athleteId, date); if (alive) setRecord(r); })();
    return () => {
      alive = false;
      if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
      const cur = recordRef.current;
      if (cur) { const next = recompute(cur); FS.saveRecord(next).then(() => { if (mountedRef.current && onSaved) onSaved(); }); }
    };
  }, [FS, planId, athleteId, date]);
  React.useEffect(() => () => {
    mountedRef.current = false;
    if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
    const cur = recordRef.current;
    if (cur) { const next = recompute(cur); FS.saveRecord(next); }
  }, []);
  const setTrial = (metricId, idx, value) => {
    setRecord(prev => {
      if (!prev) return prev;
      const results = { ...prev.results };
      const r = { ...(results[metricId] || { trials: [] }) };
      const trials = [...(r.trials || [])];
      trials[idx] = { ...(trials[idx] || {}), value, ts: Date.now() };
      r.trials = trials; results[metricId] = r;
      const next = Intake.markEdited({ ...prev, results });
      recordRef.current = next;
      return next;
    });
    scheduleSave();
  };
  const addTrial = (metricId) => {
    setRecord(prev => {
      if (!prev) return prev;
      const results = { ...prev.results };
      const r = { ...(results[metricId] || { trials: [] }) };
      const curTrials = r.trials || [];
      const target = Math.max(curTrials.length, 1) + 1;
      const trials = [...curTrials];
      while (trials.length < target) trials.push({ value: '', ts: Date.now() });
      r.trials = trials; results[metricId] = r;
      const next = Intake.markEdited({ ...prev, results });
      recordRef.current = next;
      return next;
    });
    scheduleSave();
  };
  return { record, setTrial, addTrial, flush, confirm };
}

// 站点模式中的一行：某测试项下一名运动员的多试做录入。
function FieldStationRow({ plan, athlete, test, planTests, patchAthleteMetrics, onSaved, onSaveState }) {
  const FS = window.FieldDataStore;
  const { record, setTrial, addTrial, flush, confirm } = useFieldRecord(plan.id, athlete.id, plan.date, planTests, patchAthleteMetrics, onSaved, onSaveState);
  const slice = record ? record.results[test.id] : null;
  const trials = (() => { const dt = Math.max(1, Number(test.defaultTrials) || 1); const have = (slice && slice.trials) || []; return have.length >= dt ? have : have.concat(Array.from({ length: dt - have.length }, () => ({ value: '' }))); })();
  const cb = record ? FS.computeBest(trials, test.dir) : { best: null, bestTrialIdx: -1 };
  return (
    <div className="field-station-row">
      <div className="field-station-athlete"><span>{String(athlete.name || '?').split(/\s+/).map(part => part[0]).join('').slice(0, 2)}</span><b>{athlete.name}</b></div>
      {!record ? <span style={{ fontSize: 12, color: 'var(--muted)' }}>…</span> : (
        <div className="field-station-trials">
          {trials.map((t, i) => {
            const isBest = cb.best != null && cb.bestTrialIdx === i;
            return (
              <input className="field-station-input" key={i} type="text" inputMode="decimal" value={t && t.value != null ? t.value : ''}
                onChange={(e) => setTrial(test.id, i, e.target.value)} onBlur={flush} placeholder={`第${i + 1}次`}
                style={{ width: 80, padding: '8px 9px', borderRadius: 6, textAlign: 'right', font: 'inherit', fontSize: 15, border: '1px solid ' + (isBest ? 'var(--pos, #16a34a)' : 'var(--border)'), background: isBest ? 'rgba(22,163,74,.07)' : 'var(--panel, #fff)', color: 'var(--text)' }} />
            );
          })}
          <button type="button" onClick={() => addTrial(test.id)} title="加一次试做" style={{ width: 34, height: 34, borderRadius: 6, border: '1px dashed var(--border)', background: 'transparent', color: 'var(--muted)', cursor: 'pointer', fontSize: 18, lineHeight: 1, flexShrink: 0 }}>+</button>
          <span style={{ fontSize: 12, fontWeight: 600, color: cb.best != null ? 'var(--pos, #16a34a)' : 'var(--muted)', whiteSpace: 'nowrap' }}>{cb.best != null ? `最佳 ${cb.best}${test.unit || ''}` : ''}</span>
          <button data-field-intake-confirm type="button" className="btn" disabled={cb.best == null} onClick={confirm} style={{ fontSize: 11 }}>
            {record.meta?.nativeIntake?.state === 'confirmed' && record.meta.nativeIntake.publishState === 'published' ? '已写入档案 ✓' : record.meta?.nativeIntake?.state === 'confirmed' ? '重试写入档案' : record.meta?.nativeIntake?.state === 'dirty' ? '重新确认写入' : '确认写入档案'}
          </button>
        </div>
      )}
    </div>
  );
}

// 站点模式：左测试项 / 右该项全员逐行录入。
function FieldStationMode({ plan, planAthletes, planTests, patchAthleteMetrics, onSaved, onSaveState, narrow }) {
  const [selTestId, setSelTestId] = React.useState(planTests[0] ? planTests[0].id : null);
  const selTest = planTests.find(t => t.id === selTestId);
  const dirText = selTest ? (selTest.dir === 'lower' ? '越低越好' : selTest.dir === 'higher' ? '越高越好' : '中性') : '';
  return (
    <div className={`field-station-workspace${narrow ? ' is-narrow' : ''}`}>
      {narrow ? (
        // 移动端：测试项改为横向滚动 chip,省竖向空间、拇指可滑
        <div style={{ display: 'flex', gap: 8, overflowX: 'auto', padding: '12px 14px', borderBottom: '1px solid var(--border)', background: 'var(--panel, #fff)', WebkitOverflowScrolling: 'touch' }}>
          {planTests.map(t => {
            const active = t.id === selTestId;
            return (
              <button key={t.id} type="button" onClick={() => setSelTestId(t.id)} style={{ flex: '0 0 auto', padding: '9px 14px', borderRadius: 999, border: '1px solid ' + (active ? 'var(--accent, #2563eb)' : 'var(--border)'), cursor: 'pointer', font: 'inherit', fontSize: 13, fontWeight: active ? 600 : 500, background: active ? 'var(--accent, #2563eb)' : 'var(--panel-hi, #edeae2)', color: active ? '#fff' : 'var(--text-2)', whiteSpace: 'nowrap' }}>
                {t.label}<span style={{ fontSize: 10, opacity: .7, marginLeft: 5 }}>{t.unit || ''}</span>
              </button>
            );
          })}
        </div>
      ) : (
        <aside className="field-station-tests">
          {planTests.map(t => {
            const active = t.id === selTestId;
            return (
              <button key={t.id} type="button" onClick={() => setSelTestId(t.id)} style={{ width: '100%', textAlign: 'left', padding: '11px 14px', border: 'none', borderBottom: '1px solid var(--border)', cursor: 'pointer', font: 'inherit', background: active ? 'var(--panel-2, #f1f5f9)' : 'transparent', borderLeft: '3px solid ' + (active ? 'var(--accent, #2563eb)' : 'transparent'), display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
                <span style={{ fontSize: 13, fontWeight: active ? 600 : 500, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.label}</span>
                <span style={{ fontSize: 11, color: 'var(--muted)', flexShrink: 0 }}>{t.unit || ''}</span>
              </button>
            );
          })}
        </aside>
      )}
      <div className="field-station-main">
        {!selTest ? <div style={{ color: 'var(--muted)', fontSize: 13 }}>该计划没有测试项。</div> : (
          <div>
            <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>{selTest.label} <span style={{ fontSize: 12, color: 'var(--muted)', fontWeight: 400 }}>{selTest.unit || '—'} · {dirText}</span></div>
            <div style={{ fontSize: 12, color: 'var(--muted)', margin: '2px 0 16px' }}>{plan.date} · 全队逐项录入</div>
            {planAthletes.map(a => (
              <FieldStationRow key={a.id} plan={plan} athlete={a} test={selTest} planTests={planTests} patchAthleteMetrics={patchAthleteMetrics} onSaved={onSaved} onSaveState={onSaveState} />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

// 分步录入(Stepper)：全屏一次一格（运动员×测试），大输入 + 上一个/下一个。移动端最专注、单手可用。
function FieldStepperCell({ plan, athlete, test, planTests, patchAthleteMetrics, onSaved, onSaveState, idx, total, onPrev, onNext }) {
  const FS = window.FieldDataStore;
  const { record, setTrial, addTrial, flush, confirm } = useFieldRecord(plan.id, athlete.id, plan.date, planTests, patchAthleteMetrics, onSaved, onSaveState);
  const dirText = test.dir === 'lower' ? '越低越好' : test.dir === 'higher' ? '越高越好' : '中性';
  const slice = record ? record.results[test.id] : null;
  const dt = Math.max(1, Number(test.defaultTrials) || 1);
  const have = (slice && slice.trials) || [];
  const trials = have.length >= dt ? have : have.concat(Array.from({ length: dt - have.length }, () => ({ value: '' })));
  const cb = record ? FS.computeBest(trials, test.dir) : { best: null, bestTrialIdx: -1 };
  const hist = (() => {
    if (!athlete.seasons) return null;
    const es = Object.entries(athlete.seasons).filter(([d, v]) => d !== plan.date && v && v[test.id] != null && isFinite(Number(v[test.id]))).map(([d, v]) => ({ d, v: Number(v[test.id]) }));
    if (!es.length) return null; es.sort((p, q) => p.d < q.d ? 1 : -1);
    const vals = es.map(o => o.v); return { last: es[0].v, best: test.dir === 'lower' ? Math.min(...vals) : Math.max(...vals) };
  })();
  const goNext = () => { flush(); onNext(); };
  const pct = Math.round((idx + 1) / total * 100);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', minHeight: 'min(72vh, 580px)' }}>
      <div style={{ padding: '14px 18px 0' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--muted)' }}>
          <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{plan.name}</span>
          <span style={{ fontFamily: 'var(--font-mono, monospace)', flexShrink: 0, marginLeft: 8 }}>{idx + 1} / {total}</span>
        </div>
        <div style={{ height: 6, borderRadius: 99, background: 'var(--panel-hi, #edeae2)', marginTop: 8, position: 'relative' }}>
          <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, borderRadius: 99, background: 'var(--accent, #2563eb)', width: pct + '%', transition: 'width .2s' }}/>
        </div>
        <div style={{ fontSize: 21, fontWeight: 700, color: 'var(--text)', marginTop: 14 }}>{athlete.name}</div>
        <div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 2 }}>{test.label} · {test.unit || '—'} · {dirText}</div>
      </div>
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', gap: 14, padding: '20px 18px' }}>
        {!record ? <span style={{ color: 'var(--muted)' }}>加载中…</span> : (
          <React.Fragment>
            <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'center' }}>
              {trials.map((t, i) => {
                const isBest = cb.best != null && cb.bestTrialIdx === i;
                return (
                  <div key={i} style={{ textAlign: 'center' }}>
                    <div style={{ fontSize: 10, color: 'var(--muted-2)', marginBottom: 4 }}>第{i + 1}次</div>
                    <input type="text" inputMode="decimal" value={t && t.value != null ? t.value : ''} autoFocus={i === 0}
                      onChange={e => setTrial(test.id, i, e.target.value)} onBlur={flush}
                      style={{ width: 96, height: 64, borderRadius: 14, textAlign: 'center', font: 'inherit', fontSize: 30, fontWeight: 600, fontFamily: 'var(--font-mono, monospace)',
                        border: '2px solid ' + (isBest ? 'var(--pos, #16a34a)' : 'var(--border-strong, #c9cfd9)'), background: isBest ? 'rgba(22,163,74,.07)' : 'var(--panel-2, #f7f6f2)', color: 'var(--text)' }}/>
                  </div>
                );
              })}
              <button type="button" onClick={() => addTrial(test.id)} title="加一次试做" style={{ alignSelf: 'flex-end', width: 48, height: 64, borderRadius: 14, border: '2px dashed var(--border)', background: 'transparent', color: 'var(--muted)', fontSize: 24, cursor: 'pointer' }}>+</button>
            </div>
            <div style={{ fontSize: 12, color: 'var(--muted)' }}>
              {cb.best != null && <span style={{ color: 'var(--pos, #16a34a)', fontWeight: 600 }}>最佳 {cb.best}{test.unit || ''}　</span>}
              {hist && <span>上次 {hist.last} · 历史最佳 {hist.best}</span>}
            </div>
          </React.Fragment>
        )}
      </div>
      <div style={{ display: 'flex', gap: 10, padding: '12px 18px 18px', borderTop: '1px solid var(--border)' }}>
        <button type="button" onClick={onPrev} disabled={idx === 0} style={{ flex: '0 0 92px', height: 52, borderRadius: 14, border: '1px solid var(--border)', background: 'var(--panel-hi, #edeae2)', color: 'var(--text-2)', fontSize: 15, cursor: idx === 0 ? 'default' : 'pointer', opacity: idx === 0 ? 0.5 : 1 }}>← 上一个</button>
        <button type="button" onClick={goNext} style={{ flex: 1, height: 52, borderRadius: 14, border: 'none', background: 'var(--panel-hi, #edeae2)', color: 'var(--text)', fontSize: 15, fontWeight: 600, cursor: 'pointer' }}>{idx + 1 >= total ? '保存草稿 ✓' : '保存草稿并下一个 →'}</button>
        <button data-field-intake-confirm type="button" onClick={confirm} disabled={cb.best == null} style={{ flex: 1, height: 52, borderRadius: 14, border: 'none', background: 'var(--accent, #2563eb)', color: '#fff', fontSize: 15, fontWeight: 600, cursor: cb.best == null ? 'default' : 'pointer', opacity: cb.best == null ? 0.5 : 1 }}>
          {record?.meta?.nativeIntake?.state === 'confirmed' && record.meta.nativeIntake.publishState === 'published' ? '已写入档案 ✓' : record?.meta?.nativeIntake?.state === 'confirmed' ? '重试写入档案' : record?.meta?.nativeIntake?.state === 'dirty' ? '重新确认写入' : '确认写入档案'}
        </button>
      </div>
    </div>
  );
}

function FieldStepperMode({ plan, planAthletes, planTests, patchAthleteMetrics, onSaved, onSaveState }) {
  const queue = React.useMemo(() => {
    const q = [];
    planAthletes.forEach(a => planTests.forEach(t => q.push({ athlete: a, test: t })));
    return q;
  }, [planAthletes, planTests]);
  const [idx, setIdx] = React.useState(0);
  if (!queue.length) return <div style={{ padding: 24, color: 'var(--muted)', fontSize: 13 }}>该计划没有运动员或测试项。</div>;
  const clamped = Math.max(0, Math.min(idx, queue.length - 1));
  const cur = queue[clamped];
  return (
    <div className="field-stepper-workspace">
      <FieldStepperCell
        key={cur.athlete.id}
        plan={plan} athlete={cur.athlete} test={cur.test} planTests={planTests}
        patchAthleteMetrics={patchAthleteMetrics} onSaved={onSaved} onSaveState={onSaveState}
        idx={clamped} total={queue.length}
        onPrev={() => setIdx(i => Math.max(0, i - 1))}
        onNext={() => setIdx(i => Math.min(queue.length - 1, i + 1))}
      />
    </div>
  );
}

function FieldPlanWorkspace({ plan, athletes, groups, customTests = [], patchAthleteMetrics, onBack, sourceFocus = null, initialMode = 'athlete' }) {
  const FS = window.FieldDataStore;
  const Intake = window.NativeFieldIntake;
  const planTests = (plan.testDefIds || []).map(id => fieldResolveTest(id, groups, customTests)).filter(Boolean);
  const planAthletes = (plan.athleteIds || []).map(id => athletes.find(a => a.id === id)).filter(Boolean);
  const total = planTests.length;
  const narrow = useFieldNarrow();
  const [norms, setNorms] = React.useState([]);
  React.useEffect(() => {
    let alive = true;
    (async () => { if (FS) { try { await FS.init('default'); const ns = await FS.listNorms(); if (alive) setNorms(ns); } catch (e) {} } })();
    return () => { alive = false; };
  }, [FS]);

  const focusedAthleteId = sourceFocus?.athleteId && planAthletes.some(a => a.id === sourceFocus.athleteId)
    ? sourceFocus.athleteId
    : null;
  const focusedTest = sourceFocus?.testDefId ? planTests.find(t => t.id === sourceFocus.testDefId) : null;
  const [selId, setSelId] = React.useState(focusedAthleteId || (planAthletes[0] ? planAthletes[0].id : null));
  const [record, setRecord] = React.useState(null);
  const [summary, setSummary] = React.useState({});
  const [mode, setMode] = React.useState(initialMode);
  const [saveState, setSaveState] = React.useState('idle');
  const [lastSavedAt, setLastSavedAt] = React.useState(null);
  const modeRef = React.useRef(initialMode);
  React.useEffect(() => { modeRef.current = mode; }, [mode]);
  React.useEffect(() => {
    if (['athlete', 'station', 'stepper'].includes(initialMode)) setMode(initialMode);
  }, [initialMode, plan.id]);
  React.useEffect(() => {
    if (focusedAthleteId) setSelId(focusedAthleteId);
  }, [focusedAthleteId, plan.id]);
  const recordRef = React.useRef(null);
  React.useEffect(() => { recordRef.current = record; }, [record]);

  const refreshSummary = React.useCallback(async () => {
    if (!FS) return;
    try {
      const recs = await FS.listRecordsByPlan(plan.id);
      const map = {};
      recs.forEach(r => { map[r.athleteId] = Object.values(r.results || {}).filter(x => x && x.best != null).length; });
      setSummary(map);
    } catch (e) { console.warn('refreshSummary failed', e); }
  }, [FS, plan.id]);

  React.useEffect(() => { refreshSummary(); }, [refreshSummary, mode]);

  const exportCSV = async () => {
    try {
      const recs = await FS.listRecordsByPlan(plan.id);
      const byAth = {}; recs.forEach(r => { byAth[r.athleteId] = r; });
      const statusLabel = { present: '出席', absent: '缺席', injured: '伤病', dnf: '未完成' };
      const headers = ['运动员', '状态', ...planTests.map(t => t.label + (t.unit ? '(' + t.unit + ')' : ''))];
      const rows = [headers];
      planAthletes.forEach(a => {
        const r = byAth[a.id] || {};
        rows.push([a.name, statusLabel[r.status] || '出席', ...planTests.map(t => { const res = (r.results || {})[t.id]; return res && res.best != null ? res.best : ''; })]);
      });
      const csv = rows.map(row => row.map(c => { const s = String(c); return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s; }).join(',')).join('\n');
      const blob = new Blob(['﻿' + csv], { type: 'text/csv;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a'); a.href = url; a.download = plan.name + '_' + plan.date + '.csv';
      document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
    } catch (e) { console.warn('exportCSV failed', e); window.alert('导出失败：' + e.message); }
  };

  const saveTimer = React.useRef(null);
  const mountedRef = React.useRef(true);
  const persistRef = React.useRef(null);

  // best 始终从 trials 派生：录入态只存 trials，保存/显示时再算 best。
  const recompute = (cur) => {
    const results = { ...cur.results };
    planTests.forEach(t => {
      const r = results[t.id];
      if (r && r.trials) {
        const cb = FS.computeBest(r.trials, t.dir);
        results[t.id] = { ...r, unit: t.unit, best: cb.best, bestTrialIdx: cb.bestTrialIdx };
      }
    });
    return { ...cur, results };
  };
  const persist = async () => {
    const cur = recordRef.current;
    if (!cur) return;
    const next = recompute(cur);
    recordRef.current = next;
    if (mountedRef.current) setSaveState('saving');
    try {
      await FS.saveRecord(next);
      if (mountedRef.current) {
        setSaveState('saved');
        setLastSavedAt(Date.now());
        refreshSummary();
      }
      return next;
    } catch (e) {
      if (mountedRef.current) setSaveState('error');
      throw e;
    }
  };
  persistRef.current = persist;
  const scheduleSave = () => {
    if (mountedRef.current) setSaveState('pending');
    if (saveTimer.current) clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => { saveTimer.current = null; persistRef.current(); }, 500);
  };
  const flushSave = () => {
    if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
    persistRef.current();
  };

  const confirmCurrentRecord = async () => {
    if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
    const cur = recordRef.current;
    if (!cur) return;
    setSaveState('saving');
    try {
      const next = recompute(cur);
      const retry = next.meta?.nativeIntake?.state === 'confirmed' && next.meta.nativeIntake.publishState === 'pending';
      const confirmation = retry
        ? { record: next, ...Intake.buildPublish(next) }
        : Intake.buildConfirmation({ record: next, tests: planTests, confirmedAt: new Date().toISOString() });
      if (!retry) await FS.confirmRecord(confirmation.record);
      recordRef.current = confirmation.record;
      setRecord(confirmation.record);
      if (patchAthleteMetrics) patchAthleteMetrics(confirmation.record.athleteId, confirmation.record.date, confirmation.patch, confirmation.sourceMeta);
      const published = Intake.markPublished(confirmation.record, new Date().toISOString());
      await FS.saveRecord(published);
      recordRef.current = published;
      setRecord(published);
      setSaveState('saved'); setLastSavedAt(Date.now()); refreshSummary();
    } catch (e) {
      setSaveState('error');
    }
  };

  // 加载选中运动员记录；切换运动员时先把上一位待存改动落库（防丢）。
  React.useEffect(() => {
    let alive = true;
    if (!selId) { setRecord(null); return; }
    (async () => { const r = await FS.ensureRecord(plan.id, selId, plan.date); if (alive) setRecord(r); })();
    return () => {
      alive = false;
      if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
      const cur = recordRef.current;
      if (cur && modeRef.current === 'athlete') { const next = recompute(cur); FS.saveRecord(next).then(() => { if (mountedRef.current) refreshSummary(); }); }
    };
  }, [FS, selId, plan.id, plan.date]);

  // 卸载时落库待存改动（不 setState，避免卸载告警）。
  React.useEffect(() => () => {
    mountedRef.current = false;
    if (saveTimer.current) { clearTimeout(saveTimer.current); saveTimer.current = null; }
    const cur = recordRef.current;
    if (cur && modeRef.current === 'athlete') { const next = recompute(cur); FS.saveRecord(next); }
  }, []);

  const setTrial = (metricId, idx, value) => {
    setRecord(prev => {
      if (!prev) return prev;
      const results = { ...prev.results };
      const r = { ...(results[metricId] || { trials: [] }) };
      const trials = [...(r.trials || [])];
      trials[idx] = { ...(trials[idx] || {}), value, ts: Date.now() };
      r.trials = trials;
      results[metricId] = r;
      const next = Intake.markEdited({ ...prev, results });
      recordRef.current = next;
      return next;
    });
    scheduleSave();
  };
  const addTrial = (metricId) => {
    setRecord(prev => {
      if (!prev) return prev;
      const results = { ...prev.results };
      const r = { ...(results[metricId] || { trials: [] }) };
      const curTrials = r.trials || [];
      const target = Math.max(curTrials.length, 1) + 1;
      const trials = [...curTrials];
      while (trials.length < target) trials.push({ value: '', ts: Date.now() });
      r.trials = trials;
      results[metricId] = r;
      const next = Intake.markEdited({ ...prev, results });
      recordRef.current = next;
      return next;
    });
    scheduleSave();
  };
  const setStatus = async (s) => {
    const cur = recordRef.current;
    if (!cur) return;
    const next = Intake.markEdited({ ...cur, status: s });
    recordRef.current = next;
    setRecord(next);
    setSaveState('saving');
    try {
      await FS.saveRecord(next);
      setSaveState('saved');
      setLastSavedAt(Date.now());
    } catch (e) {
      setSaveState('error');
      throw e;
    }
  };
  // 该运动员该指标的历史参考（排除本场次日期）：上次 + 历史最佳。
  const historyFor = (metricId, dir) => {
    const a = planAthletes.find(x => x.id === selId);
    if (!a || !a.seasons) return null;
    const entries = Object.entries(a.seasons)
      .filter(([d, vals]) => d !== plan.date && vals && vals[metricId] != null && isFinite(Number(vals[metricId])))
      .map(([d, vals]) => ({ d, v: Number(vals[metricId]) }));
    if (!entries.length) return null;
    entries.sort((p, q) => (p.d < q.d ? 1 : -1));
    const vals = entries.map(o => o.v);
    return { last: entries[0].v, best: dir === 'lower' ? Math.min(...vals) : Math.max(...vals) };
  };

  const selAthlete = planAthletes.find(a => a.id === selId);
  const hasConfirmableResults = !!record && planTests.some(test => FS.computeBest(record.results?.[test.id]?.trials || [], test.dir).best != null);
  const handleChildSaveState = React.useCallback((state) => {
    if (state === 'saved') setLastSavedAt(Date.now());
    setSaveState(state);
  }, []);
  const handleChildSaved = React.useCallback(() => {
    setSaveState('saved');
    setLastSavedAt(Date.now());
    refreshSummary();
  }, [refreshSummary]);

  return (
    <div className="field-plan-workspace">
      <div className="field-entry-toolbar">
        <button type="button" className="btn ghost" onClick={onBack} style={{ fontSize: 13 }}>← 计划列表</button>
        <div style={{ minWidth: 0, flex: 1 }}>
          <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>{plan.name}</span>
          <span style={{ fontSize: 12, color: 'var(--muted)', marginLeft: 10 }}>{plan.date} · {total} 测试项 · {planAthletes.length} 人</span>
        </div>
        <button type="button" className="btn ghost" onClick={exportCSV} style={{ fontSize: 13 }}>导出 CSV</button>
        <FieldAutosaveStatus state={saveState} lastSavedAt={lastSavedAt}/>
        <div className="field-entry-modes">
          {[['athlete', '按运动员'], ['station', '工位'], ['stepper', '分步']].map(([v, t]) => {
            const on = mode === v;
            return <button key={v} type="button" onClick={() => setMode(v)} style={{ padding: '6px 12px', border: 'none', cursor: 'pointer', fontSize: 12, font: 'inherit', background: on ? 'var(--accent, #2563eb)' : 'var(--panel, #fff)', color: on ? '#fff' : 'var(--text-2)' }}>{t}</button>;
          })}
        </div>
      </div>

      {mode === 'stepper' ? (
        <FieldStepperMode plan={plan} planAthletes={planAthletes} planTests={planTests} patchAthleteMetrics={patchAthleteMetrics} onSaved={handleChildSaved} onSaveState={handleChildSaveState} />
      ) : mode === 'station' ? (
        <FieldStationMode plan={plan} planAthletes={planAthletes} planTests={planTests} patchAthleteMetrics={patchAthleteMetrics} onSaved={handleChildSaved} onSaveState={handleChildSaveState} narrow={narrow} />
      ) : (
      <div style={{ display: 'flex', flexDirection: narrow ? 'column' : 'row', alignItems: 'stretch', minWidth: 0 }}>
        {/* 左：名单 */}
        <div style={{ width: narrow ? '100%' : 220, flexShrink: 0, borderRight: narrow ? 'none' : '1px solid var(--border)', borderBottom: narrow ? '1px solid var(--border)' : 'none', maxHeight: narrow ? 220 : 'none', overflowY: narrow ? 'auto' : 'visible', background: 'var(--panel, #fff)' }}>
          {planAthletes.map(a => {
            const done = summary[a.id] || 0;
            const active = a.id === selId;
            const sourceFocused = focusedAthleteId === a.id;
            const complete = total > 0 && done >= total;
            return (
              <button key={a.id} type="button" data-field-source-athlete-focus={sourceFocused ? 'true' : undefined} onClick={() => setSelId(a.id)} style={{
                width: '100%', textAlign: 'left', padding: '11px 14px', border: 'none',
                borderBottom: '1px solid var(--border)', cursor: 'pointer', font: 'inherit',
                background: sourceFocused ? 'rgba(37, 99, 235, .08)' : (active ? 'var(--panel-2, #f1f5f9)' : 'transparent'),
                borderLeft: '3px solid ' + (sourceFocused || active ? 'var(--accent, #2563eb)' : 'transparent'),
                display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
              }}>
                <span style={{ fontSize: 13, fontWeight: active ? 600 : 500, color: 'var(--text)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{a.name}</span>
                <span style={{ fontSize: 11, fontWeight: 600, color: complete ? 'var(--pos, #16a34a)' : 'var(--muted)', flexShrink: 0 }}>{done}/{total}</span>
              </button>
            );
          })}
        </div>

        {/* 右：录入 */}
        <div style={{ flex: 1, minWidth: 0, padding: 20 }}>
          {!selId ? (
            <div style={{ color: 'var(--muted)', fontSize: 13 }}>该计划没有运动员。</div>
          ) : !record ? (
            <div style={{ color: 'var(--muted)', fontSize: 13 }}>加载中…</div>
          ) : (
            <div style={{ maxWidth: 560 }}>
              <div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)' }}>{selAthlete ? selAthlete.name : selId}</div>
              <div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 2 }}>{plan.date}</div>
              <div data-field-intake-confirmation style={{ marginTop: 10, padding: '10px 12px', border: '1px solid var(--border)', borderRadius: 8, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', background: 'var(--panel-2)' }}>
                <div style={{ flex: 1, minWidth: 180 }}>
                  <b style={{ display: 'block', fontSize: 12, color: 'var(--text)' }}>档案写入确认</b>
                  <span style={{ fontSize: 11, color: 'var(--muted)' }}>
                    {record.meta?.nativeIntake?.state === 'confirmed'
                      ? record.meta.nativeIntake.publishState === 'published'
                        ? `已确认并写入档案 · ${Object.keys(record.meta.nativeIntake.confirmedMetrics || {}).length} 项 · 修订 ${record.meta.nativeIntake.confirmationRevision}`
                        : '结果已确认，但档案写入未完成；可安全重试。'
                      : record.meta?.nativeIntake?.state === 'dirty'
                        ? '已确认结果发生修改；档案仍保留上次确认值，需重新确认。'
                        : '当前仅为自动保存草稿，不会进入纵向档案。'}
                  </span>
                </div>
                <button data-field-intake-confirm type="button" className="btn primary" onClick={confirmCurrentRecord}
                  disabled={!hasConfirmableResults}>
                  {record.meta?.nativeIntake?.state === 'confirmed' && record.meta.nativeIntake.publishState === 'published' ? '再次确认写入' : record.meta?.nativeIntake?.state === 'confirmed' ? '重试写入档案' : record.meta?.nativeIntake?.state === 'dirty' ? '重新确认写入档案' : '预览并确认写入档案'}
                </button>
              </div>
              {sourceFocus ? (
                <div data-field-source-focus="true" style={{
                  marginTop: 8,
                  padding: '7px 9px',
                  border: '1px solid rgba(37, 99, 235, .22)',
                  borderRadius: 7,
                  background: 'rgba(37, 99, 235, .06)',
                  color: 'var(--text-2)',
                  fontSize: 11.5,
                }}>
                  Source focus · {focusedTest ? focusedTest.label : 'Recorded test'}{sourceFocus.assessmentId ? ` · ${sourceFocus.assessmentId}` : ''}
                </div>
              ) : null}
              <div style={{ display: 'flex', gap: 6, margin: '10px 0 16px', flexWrap: 'wrap' }}>
                {[['present', '出席'], ['absent', '缺席'], ['injured', '伤病'], ['dnf', '未完成']].map(([v, t]) => {
                  const on = (record.status || 'present') === v;
                  return (
                    <button key={v} type="button" onClick={() => setStatus(v)} style={{
                      padding: '5px 11px', borderRadius: 999, fontSize: 12, cursor: 'pointer', font: 'inherit',
                      border: '1px solid ' + (on ? 'var(--accent, #2563eb)' : 'var(--border)'),
                      background: on ? 'var(--accent, #2563eb)' : 'transparent', color: on ? '#fff' : 'var(--text-2)',
                    }}>{t}</button>
                  );
                })}
              </div>
              <div style={{ opacity: (record.status && record.status !== 'present') ? 0.45 : 1, transition: 'opacity .15s' }}>
                {(record.status && record.status !== 'present') ? (
                  <div style={{ fontSize: 12, color: 'var(--warn, #d97706)', marginBottom: 8 }}>
                    已标记「{({ absent: '缺席', injured: '伤病', dnf: '未完成' })[record.status]}」——如仍需录入可直接填写。
                  </div>
                ) : null}
                {planTests.map(test => (
                  <FieldTestCard
                    key={test.id}
                    test={test}
                    slice={record.results[test.id]}
                    history={historyFor(test.id, test.dir)}
                    norm={FS.matchNorm(test.id, selAthlete, norms)}
                    onTrial={setTrial}
                    onAddTrial={addTrial}
                    onCommit={flushSave}
                    focused={sourceFocus?.testDefId === test.id}
                  />
                ))}
              </div>
            </div>
          )}
        </div>
      </div>
      )}
    </div>
  );
}

function FieldMetricPromotionModal({ source, groups, onConfirm, onClose }) {
  const [groupId, setGroupId] = React.useState(groups[0]?.id || '');
  const [label, setLabel] = React.useState(source.label || '');
  const [unit, setUnit] = React.useState(source.unit || '');
  const [dir, setDir] = React.useState(source.dir || 'higher');
  const [allowDuplicate, setAllowDuplicate] = React.useState(false);
  const duplicate = groups.some(group => (group.metrics || []).some(metric => (
    (metric.label || '').trim().toLowerCase() === label.trim().toLowerCase()
  )));
  const valid = groupId && label.trim() && ['higher', 'lower', 'neutral'].includes(dir)
    && (!duplicate || allowDuplicate);
  return (
    <div data-field-metric-promotion-modal onClick={onClose} style={{ position: 'fixed', inset: 0, zIndex: 2200, background: 'rgba(15,23,42,.52)', backdropFilter: 'blur(5px)', display: 'grid', placeItems: 'center', padding: 20 }}>
      <div onClick={event => event.stopPropagation()} role="dialog" aria-modal="true" aria-labelledby="field-promotion-title" style={{ width: 'min(560px,96vw)', background: 'var(--panel)', border: '1px solid var(--border-strong)', borderRadius: 12, boxShadow: '0 24px 70px rgba(15,23,42,.28)', overflow: 'hidden' }}>
        <div style={{ padding: '15px 18px', borderBottom: '1px solid var(--border)' }}>
          <div id="field-promotion-title" style={{ fontSize: 15, fontWeight: 650, color: 'var(--text)' }}>提升为正式指标</div>
          <div style={{ marginTop: 4, fontSize: 11.5, color: 'var(--muted)', lineHeight: 1.5 }}>仅复制“{source.label || source.id}”的定义到指标字典；现有测试记录不会迁移、改写或回填。</div>
        </div>
        <div style={{ padding: 18, display: 'grid', gap: 12 }}>
          <label><span style={{ display: 'block', marginBottom: 5, fontSize: 11, color: 'var(--muted)' }}>目标分组</span><select value={groupId} onChange={event => setGroupId(event.target.value)} style={{ width: '100%' }}>
            {groups.map(group => <option key={group.id} value={group.id}>{group.label || group.id}</option>)}
          </select></label>
          <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.6fr) minmax(100px,.7fr)', gap: 10 }}>
            <label><span style={{ display: 'block', marginBottom: 5, fontSize: 11, color: 'var(--muted)' }}>正式名称</span><input value={label} onChange={event => { setLabel(event.target.value); setAllowDuplicate(false); }} style={{ width: '100%' }}/></label>
            <label><span style={{ display: 'block', marginBottom: 5, fontSize: 11, color: 'var(--muted)' }}>单位</span><input value={unit} onChange={event => setUnit(event.target.value)} style={{ width: '100%' }}/></label>
          </div>
          <label><span style={{ display: 'block', marginBottom: 5, fontSize: 11, color: 'var(--muted)' }}>方向</span><select value={dir} onChange={event => setDir(event.target.value)} style={{ width: '100%' }}>
            <option value="higher">越高越好</option><option value="lower">越低越好</option><option value="neutral">中性</option>
          </select></label>
          {duplicate && <label style={{ display: 'flex', alignItems: 'flex-start', gap: 8, padding: '9px 10px', borderRadius: 7, background: 'color-mix(in srgb, var(--warn) 9%, transparent)', border: '1px solid color-mix(in srgb, var(--warn) 30%, transparent)', fontSize: 11.5, color: 'var(--text-2)' }}>
            <input type="checkbox" checked={allowDuplicate} onChange={event => setAllowDuplicate(event.target.checked)}/>
            <span>已有同名正式指标；确认仍要创建一个新的独立定义。</span>
          </label>}
        </div>
        <div style={{ padding: '12px 18px', borderTop: '1px solid var(--border)', display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
          <button className="btn" onClick={onClose}>取消</button>
          <button className="btn primary" disabled={!valid} onClick={() => onConfirm({ groupId, label: label.trim(), unit: unit.trim(), dir })}>确认创建定义</button>
        </div>
      </div>
    </div>
  );
}

function FieldTestView({ athletes = [], groups = [], onGroupsChange, seasons = [], patchAthleteMetrics,
                         autoBuild = false, onConsumeAutoBuild, initialPlanId = null, onConsumeInitialPlan,
                         initialSourceFocus = null, onConsumeInitialSourceFocus }) {
  const FS = window.FieldDataStore;
  const [dbReady, setDbReady] = React.useState(null);
  const [plans, setPlans] = React.useState([]);
  const [activePlanId, setActivePlanId] = React.useState(null);
  const [building, setBuilding] = React.useState(false);
  const [planDraft, setPlanDraft] = React.useState(null);
  const [customTests, setCustomTests] = React.useState([]);
  const [templates, setTemplates] = React.useState([]);
  const [planProgress, setPlanProgress] = React.useState({});
  const [sourceFocus, setSourceFocus] = React.useState(null);
  const [promotionSource, setPromotionSource] = React.useState(null);
  const [promotionNotice, setPromotionNotice] = React.useState('');
  const [requestedEntryMode, setRequestedEntryMode] = React.useState('athlete');

  // Item 4: consume one-shot routing signals from calendar / source re-entry.
  //   autoBuild → open builder · initialPlanId → open plan · initialSourceFocus → open plan and focus source record.
  React.useEffect(() => {
    if (autoBuild) { setBuilding(true); onConsumeAutoBuild && onConsumeAutoBuild(); }
  }, [autoBuild, onConsumeAutoBuild]);
  React.useEffect(() => {
    if (initialSourceFocus?.planId) {
      setSourceFocus(initialSourceFocus);
      setActivePlanId(initialSourceFocus.planId);
      onConsumeInitialPlan && onConsumeInitialPlan();
      onConsumeInitialSourceFocus && onConsumeInitialSourceFocus();
      return;
    }
    if (initialPlanId) {
      setSourceFocus(null);
      setActivePlanId(initialPlanId);
      onConsumeInitialPlan && onConsumeInitialPlan();
    }
  }, [initialSourceFocus, initialPlanId, onConsumeInitialPlan, onConsumeInitialSourceFocus]);

  const refreshPlans = React.useCallback(async () => {
    if (!FS) return;
    try {
      const list = await FS.listPlans();
      setPlans(list);
      const prog = {};
      for (const p of list) {
        const recs = await FS.listRecordsByPlan(p.id);
        const entered = recs.filter(r => Object.values(r.results || {}).some(x => x && x.best != null)).length;
        prog[p.id] = { entered, total: (p.athleteIds || []).length };
      }
      setPlanProgress(prog);
    } catch (e) { console.warn('listPlans failed', e); }
  }, [FS]);
  const refreshCustomTests = React.useCallback(async () => {
    if (!FS) return;
    try { setCustomTests(await FS.listTestDefs()); } catch (e) { console.warn('listTestDefs failed', e); }
  }, [FS]);
  const refreshTemplates = React.useCallback(async () => {
    if (!FS) return;
    try { setTemplates(await FS.listTemplates()); } catch (e) { console.warn('listTemplates failed', e); }
  }, [FS]);

  React.useEffect(() => {
    let alive = true;
    (async () => {
      const ok = FS ? await FS.healthCheck() : false;
      if (!alive) return;
      setDbReady(ok);
      if (ok) { await FS.init('default'); if (alive) { await refreshPlans(); await refreshCustomTests(); await refreshTemplates(); } }
    })();
    return () => { alive = false; };
  }, [FS, refreshPlans, refreshCustomTests, refreshTemplates]);

  const handleCreateTest = async (def) => {
    if (!FS) return null;
    const created = await FS.saveTestDef(def);
    await refreshCustomTests();
    return created;
  };

  const handlePromoteTest = (testId) => {
    const source = customTests.find(t => t.id === testId);
    if (!source) return;
    if (typeof onGroupsChange !== 'function') {
      window.alert('Settings / Metrics 暂不可写入，无法提升为正式指标。');
      return;
    }
    if (!groups.length) {
      window.alert('没有可用的 Settings / Metrics 分组。');
      return;
    }
    setPromotionSource(source);
    setPromotionNotice('');
  };

  const confirmPromoteTest = ({ groupId, label, unit, dir }) => {
    const groupIdx = groups.findIndex(group => group.id === groupId);
    if (groupIdx < 0 || !promotionSource) return;
    const idBase = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'field_metric';
    const newMetric = {
      id: `field_${idBase}_${Date.now()}`,
      label,
      unit,
      dir,
      worst: 0,
      avg: 0,
      best: 0,
    };
    onGroupsChange(groups.map((g, i) => i === groupIdx ? { ...g, metrics: [...(g.metrics || []), newMetric] } : g));
    setPromotionSource(null);
    setPromotionNotice(`已创建正式指标“${label}”；原测试项和历史记录保持不变。`);
  };

  const activePlan = plans.find(p => p.id === activePlanId) || null;
  const fieldTestMode = building ? 'builder' : activePlan ? 'entry' : 'queue';

  const handleSavePlan = async (draft) => {
    const p = await FS.createPlan(draft);
    await refreshPlans();
    setActivePlanId(p.id);
    setBuilding(false);
    setPlanDraft(null);
  };
  const handleNewPlan = () => {
    setPlanDraft(null);
    setBuilding(true);
  };
  const handleShowQueue = () => {
    setSourceFocus(null);
    setBuilding(false);
    setPlanDraft(null);
    setActivePlanId(null);
  };
  const handleOpenEntryMode = (mode) => {
    const planId = activePlanId || (plans[0] && plans[0].id);
    if (!planId) {
      handleNewPlan();
      return;
    }
    setRequestedEntryMode(mode);
    setBuilding(false);
    setActivePlanId(planId);
  };
  const handleNewFromTemplate = (templateId) => {
    const template = templates.find(t => t.id === templateId);
    if (!template) return;
    setPlanDraft({
      name: template.name || '',
      date: new Date().toISOString().slice(0, 10),
      testDefIds: [...(template.testDefIds || [])],
      athleteIds: [...(template.athleteIds || [])],
      sourceTemplateName: template.name || '未命名模板',
    });
    setBuilding(true);
  };
  const handleSaveTemplate = async (plan) => {
    if (!FS || !plan) return;
    const name = window.prompt('模板名称', plan.name || 'Field Test 模板');
    if (!name || !name.trim()) return;
    const trimmed = name.trim();
    const duplicate = templates.some(t => (t.name || '').trim().toLowerCase() === trimmed.toLowerCase());
    if (duplicate && !window.confirm('已有同名模板，仍然另存为新模板？')) return;
    await FS.saveTemplate({
      name: trimmed,
      testDefIds: [...(plan.testDefIds || [])],
      athleteIds: [...(plan.athleteIds || [])],
      notes: 'Created from Field Test plan setup. No results copied.',
    });
    await refreshTemplates();
  };
  const handleRenameTemplate = async (templateId) => {
    if (!FS) return;
    const template = templates.find(t => t.id === templateId);
    if (!template) return;
    const name = window.prompt('模板名称', template.name || '未命名模板');
    if (!name || !name.trim()) return;
    const trimmed = name.trim();
    const duplicate = templates.some(t => t.id !== templateId && (t.name || '').trim().toLowerCase() === trimmed.toLowerCase());
    if (duplicate && !window.confirm('已有同名模板，仍然重命名？')) return;
    await FS.saveTemplate({ ...template, name: trimmed });
    await refreshTemplates();
  };
  const handleDeleteTemplate = async (templateId) => {
    if (!FS) return;
    const template = templates.find(t => t.id === templateId);
    if (!template) return;
    if (!window.confirm('删除模板「' + (template.name || '未命名模板') + '」？已有测试计划和录入数据不会被删除。')) return;
    await FS.deleteTemplate(templateId);
    await refreshTemplates();
  };
  const handleDeletePlan = async (id) => {
    if (!window.confirm('删除该计划及其所有录入数据？此操作不可撤销。')) return;
    await FS.deletePlan(id);
    if (activePlanId === id) setActivePlanId(null);
    await refreshPlans();
  };

  let bodyContent;
  if (building) {
    bodyContent = <FieldPlanBuilder groups={groups} athletes={athletes} customTests={customTests} initialDraft={planDraft} onCreateTest={handleCreateTest} onPromoteTest={handlePromoteTest} onSave={handleSavePlan} onCancel={() => { setBuilding(false); setPlanDraft(null); }} />;
  } else if (activePlan) {
    bodyContent = <FieldPlanWorkspace plan={activePlan} athletes={athletes} groups={groups} customTests={customTests} patchAthleteMetrics={patchAthleteMetrics} sourceFocus={sourceFocus?.planId === activePlan.id ? sourceFocus : null} initialMode={requestedEntryMode} onBack={handleShowQueue} />;
  } else {
    bodyContent = <FieldPlanList plans={plans} templates={templates} progress={planProgress} groups={groups} athletes={athletes} customTests={customTests} onOpen={(id) => { setRequestedEntryMode('athlete'); setActivePlanId(id); }} onNew={handleNewPlan} onNewFromTemplate={handleNewFromTemplate} onSaveTemplate={handleSaveTemplate} onRenameTemplate={handleRenameTemplate} onDeleteTemplate={handleDeleteTemplate} onDelete={handleDeletePlan} />;
  }

  return (
    <main className="field-test-workbench">
      <FieldTestContextBar
        mode={fieldTestMode}
        activePlan={activePlan}
        entryMode={requestedEntryMode}
        dbReady={dbReady}
        onQueue={handleShowQueue}
        onBuilder={handleNewPlan}
        onEntryMode={handleOpenEntryMode}
      />
      {promotionNotice && <div data-field-promotion-notice role="status" style={{ margin: '10px 20px 0', padding: '8px 10px', borderRadius: 7, background: 'color-mix(in srgb, var(--pos) 8%, transparent)', border: '1px solid color-mix(in srgb, var(--pos) 28%, transparent)', color: 'var(--text-2)', fontSize: 11.5 }}>{promotionNotice}</div>}
      <FieldWorkspaceFrame
        mode={fieldTestMode}
      >
        {bodyContent}
      </FieldWorkspaceFrame>
      {promotionSource && <FieldMetricPromotionModal key={promotionSource.id} source={promotionSource} groups={groups} onConfirm={confirmPromoteTest} onClose={() => setPromotionSource(null)}/>}
    </main>
  );
}
