// injury.jsx v14 - Axis 损伤筛查与康复训练工作台（共享病例 · 五个子流程）
// 数据层：window.FieldDataStore.injuries（登记 + pathologyReports[] + rehab[] + assessments[] 内联）。
// 入口：损伤筛查与康复训练路由共享同一运动员病例、处方、评估与附件记录。
// 参考 Kitman Labs / Smartabase (Teamworks) 的 RTP Tracker。文案 English-first，留 i18n。

const { useState: ijS, useEffect: ijE, useMemo: ijM } = React;

// ── constants ───────────────────────────────────────────────────────────────
const IJ_PHASES = [
  { id: 'acute',     label: 'Acute 急性' },
  { id: 'subacute',  label: 'Subacute 亚急性' },
  { id: 'recovery',  label: 'Recovery 恢复期' },
  { id: 'rtt',       label: 'Return to Train 重返训练' },
  { id: 'rtp',       label: 'Return to Play 重返赛场' },
];
const IJ_PHASE_LABEL = Object.fromEntries(IJ_PHASES.map(p => [p.id, p.label]));
const IJ_STATUS_META = {
  available:   { label: 'Available 可用',     color: '#16a34a' },
  modified:    { label: 'Modified 限制',       color: '#d97706' },
  unavailable: { label: 'Unavailable 不可用',  color: '#dc2626' },
};
const IJ_TYPES = ['Strain 拉伤', 'Sprain 扭伤', 'Contusion 挫伤', 'Tendinopathy 肌腱病', 'Fracture 骨折', 'Ligament 韧带', 'Overuse 劳损', 'Other 其它'];
const IJ_MECHANISMS = [['non-contact', '非接触 Non-contact'], ['contact', '接触 Contact'], ['overuse', '劳损 Overuse'], ['unknown', '未知 Unknown']];

// Front/back muscle-group body map (viewBox 0 0 150 330). Anatomical side stored explicitly:
// FRONT view mirrors (athlete's right limb shown on viewer-left); BACK view is direct.
const IJ_VIEWS = {
  front: [
    { id: 'Head',       side: null,    s: 'e', cx: 75, cy: 24, rx: 16, ry: 19 },
    { id: 'Neck',       side: null,    s: 'r', x: 67, y: 41, w: 16, h: 11 },
    { id: 'Shoulder',   side: 'right', s: 'e', cx: 41, cy: 60, rx: 10, ry: 8 },
    { id: 'Shoulder',   side: 'left',  s: 'e', cx: 109, cy: 60, rx: 10, ry: 8 },
    { id: 'Chest',      side: null,    s: 'r', x: 49, y: 55, w: 52, h: 34 },
    { id: 'Abdomen',    side: null,    s: 'r', x: 53, y: 90, w: 44, h: 38 },
    { id: 'Upper Arm',  side: 'right', s: 'r', x: 28, y: 58, w: 14, h: 42 },
    { id: 'Upper Arm',  side: 'left',  s: 'r', x: 108, y: 58, w: 14, h: 42 },
    { id: 'Forearm',    side: 'right', s: 'r', x: 27, y: 100, w: 12, h: 40 },
    { id: 'Forearm',    side: 'left',  s: 'r', x: 111, y: 100, w: 12, h: 40 },
    { id: 'Hand',       side: 'right', s: 'e', cx: 33, cy: 146, rx: 8, ry: 9 },
    { id: 'Hand',       side: 'left',  s: 'e', cx: 117, cy: 146, rx: 8, ry: 9 },
    { id: 'Hip/Groin',  side: null,    s: 'r', x: 53, y: 129, w: 44, h: 20 },
    { id: 'Quad',       side: 'right', s: 'r', x: 53, y: 150, w: 21, h: 66 },
    { id: 'Quad',       side: 'left',  s: 'r', x: 76, y: 150, w: 21, h: 66 },
    { id: 'Knee',       side: 'right', s: 'e', cx: 63, cy: 222, rx: 10, ry: 9 },
    { id: 'Knee',       side: 'left',  s: 'e', cx: 87, cy: 222, rx: 10, ry: 9 },
    { id: 'Shin',       side: 'right', s: 'r', x: 55, y: 230, w: 16, h: 54 },
    { id: 'Shin',       side: 'left',  s: 'r', x: 79, y: 230, w: 16, h: 54 },
    { id: 'Ankle/Foot', side: 'right', s: 'e', cx: 63, cy: 294, rx: 10, ry: 8 },
    { id: 'Ankle/Foot', side: 'left',  s: 'e', cx: 87, cy: 294, rx: 10, ry: 8 },
  ],
  back: [
    { id: 'Head',        side: null,    s: 'e', cx: 75, cy: 24, rx: 16, ry: 19 },
    { id: 'Neck',        side: null,    s: 'r', x: 67, y: 41, w: 16, h: 11 },
    { id: 'Shoulder',    side: 'left',  s: 'e', cx: 41, cy: 60, rx: 10, ry: 8 },
    { id: 'Shoulder',    side: 'right', s: 'e', cx: 109, cy: 60, rx: 10, ry: 8 },
    { id: 'Upper Back',  side: null,    s: 'r', x: 49, y: 55, w: 52, h: 34 },
    { id: 'Lower Back',  side: null,    s: 'r', x: 53, y: 90, w: 44, h: 30 },
    { id: 'Upper Arm',   side: 'left',  s: 'r', x: 28, y: 58, w: 14, h: 42 },
    { id: 'Upper Arm',   side: 'right', s: 'r', x: 108, y: 58, w: 14, h: 42 },
    { id: 'Forearm',     side: 'left',  s: 'r', x: 27, y: 100, w: 12, h: 40 },
    { id: 'Forearm',     side: 'right', s: 'r', x: 111, y: 100, w: 12, h: 40 },
    { id: 'Hand',        side: 'left',  s: 'e', cx: 33, cy: 146, rx: 8, ry: 9 },
    { id: 'Hand',        side: 'right', s: 'e', cx: 117, cy: 146, rx: 8, ry: 9 },
    { id: 'Glute',       side: null,    s: 'r', x: 52, y: 122, w: 46, h: 26 },
    { id: 'Hamstring',   side: 'left',  s: 'r', x: 53, y: 150, w: 21, h: 66 },
    { id: 'Hamstring',   side: 'right', s: 'r', x: 76, y: 150, w: 21, h: 66 },
    { id: 'Knee',        side: 'left',  s: 'e', cx: 63, cy: 222, rx: 10, ry: 9 },
    { id: 'Knee',        side: 'right', s: 'e', cx: 87, cy: 222, rx: 10, ry: 9 },
    { id: 'Calf',        side: 'left',  s: 'r', x: 55, y: 230, w: 16, h: 54 },
    { id: 'Calf',        side: 'right', s: 'r', x: 79, y: 230, w: 16, h: 54 },
    { id: 'Ankle/Foot',  side: 'left',  s: 'e', cx: 63, cy: 294, rx: 10, ry: 8 },
    { id: 'Ankle/Foot',  side: 'right', s: 'e', cx: 87, cy: 294, rx: 10, ry: 8 },
  ],
};
const ijRegKey = (r) => r.id + (r.side ? '|' + r.side : '');

const ijInput = { width: '100%', padding: '6px 9px', background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 4, color: 'var(--text)', font: '13px var(--font-sans)', outline: 'none' };
function IjLabel({ children, style }) { return <div style={{ fontSize: 9.5, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em', fontWeight: 600, ...style }}>{children}</div>; }
function ijUid(p) { return (p || '') + (crypto?.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2, 8)); }
function ijToday() { return new Date().toISOString().slice(0, 10); }
function ijDaysBetween(a, b) { return Math.round((new Date(b + 'T00:00:00') - new Date(a + 'T00:00:00')) / 86400000); }

// ── BodyMap (front/back muscle-group, clickable, L/R aware) ─────────────────
const IJ_BACK_PARTS = new Set(['Hamstring', 'Glute', 'Calf', 'Upper Back', 'Lower Back']);
function BodyMap({ value, onPick }) {
  const [view, setView] = ijS(() => (value?.part && IJ_BACK_PARTS.has(value.part) ? 'back' : 'front'));
  const regions = IJ_VIEWS[view];
  const selKey = value && value.part ? value.part + (value.side ? '|' + value.side : '') : null;
  return (
    <div>
      <div style={{ display: 'flex', gap: 4, justifyContent: 'center', marginBottom: 6 }}>
        {[['front', '正面 Front'], ['back', '背面 Back']].map(([v, lbl]) => (
          <button key={v} type="button" onClick={() => setView(v)} style={{
            padding: '3px 11px', borderRadius: 6, cursor: 'pointer', fontSize: 10.5,
            background: view === v ? 'var(--accent)' : 'var(--panel-hi)', color: view === v ? '#fff' : 'var(--text-2)',
            border: `1px solid ${view === v ? 'var(--accent)' : 'var(--border)'}`,
          }}>{lbl}</button>
        ))}
      </div>
      <svg viewBox="0 0 150 330" width="118" height="260" style={{ display: 'block', margin: '0 auto' }}>
        {regions.map((r, i) => {
          const on = selKey === ijRegKey(r);
          const common = {
            fill: on ? 'var(--neg)' : 'var(--panel-hi)', fillOpacity: on ? 0.5 : 1,
            stroke: on ? 'var(--neg)' : 'var(--border-strong)', strokeWidth: 1,
            cursor: 'pointer', onClick: () => onPick({ part: r.id, side: r.side, view }),
          };
          const title = `${r.id}${r.side ? ` (${r.side})` : ''}`;
          return r.s === 'e'
            ? <ellipse key={i} cx={r.cx} cy={r.cy} rx={r.rx} ry={r.ry} {...common}><title>{title}</title></ellipse>
            : <rect key={i} x={r.x} y={r.y} width={r.w} height={r.h} rx={5} {...common}><title>{title}</title></rect>;
        })}
      </svg>
    </div>
  );
}

// ── InjuryView (shell: athlete injury list + case file) ──────────────────────
function InjuryView({ athlete, groups = [], seasons = [], patchAthleteMetrics, initialInjuryId, workflowMode = 'screening', onBack }) {
  const FS = window.FieldDataStore;
  const [injuries, setInjuries] = ijS([]);
  const [exercises, setExercises] = ijS([]);
  const [activeId, setActiveId] = ijS(initialInjuryId && initialInjuryId !== 'new' ? initialInjuryId : null);
  const [creating, setCreating] = ijS(initialInjuryId === 'new');
  const [query, setQuery] = ijS('');
  const [statusFilter, setStatusFilter] = ijS('all');
  const [phaseFilter, setPhaseFilter] = ijS('all');

  const reload = React.useCallback(async () => {
    if (!FS) return;
    try { await FS.init('default'); setInjuries(await FS.listInjuriesByAthlete(athlete.id)); setExercises(await FS.listExercises()); }
    catch (e) { console.warn('InjuryView load', e); }
  }, [FS, athlete?.id]);
  ijE(() => { reload(); }, [reload]);

  const active = injuries.find(i => i.id === activeId) || null;
  const visibleInjuries = injuries.filter(inj => {
    const q = query.trim().toLowerCase();
    const matchesQuery = !q || [inj.bodyPart, inj.side, inj.type, inj.status, IJ_PHASE_LABEL[inj.phase]]
      .some(value => String(value || '').toLowerCase().includes(q));
    const matchesStatus = statusFilter === 'all'
      || (statusFilter === 'active' && inj.status !== 'available')
      || (statusFilter === 'available' && inj.status === 'available');
    const matchesPhase = phaseFilter === 'all' || inj.phase === phaseFilter;
    return matchesQuery && matchesStatus && matchesPhase;
  });

  const saveInjury = async (inj) => { const x = await FS.saveInjury({ athleteId: athlete.id, ...inj }); await reload(); setActiveId(x.id); setCreating(false); return x; };
  const removeInjury = async (id) => { if (!window.confirm('删除该伤病档案及其康复/评估记录？')) return; await FS.deleteInjury(id); setActiveId(null); await reload(); };

  let caseContent;
  if (creating) {
    caseContent = (
      <div className="injury-case-surface injury-create-surface">
        <div className="injury-case-empty-head">
          <IjLabel>NEW CASE</IjLabel>
          <h2>登记损伤</h2>
          <p>选择部位并记录当下已知事实；后续康复与评估继续写入同一病例。</p>
        </div>
        <OverviewTab injury={{ athleteId: athlete.id, date: ijToday(), status: 'unavailable', phase: 'acute' }} isNew onSave={saveInjury} onCancel={() => setCreating(false)} />
      </div>
    );
  } else if (active) {
    caseContent = <InjuryCase injury={active} athlete={athlete} groups={groups} seasons={seasons} exercises={exercises}
              workflowMode={workflowMode}
              patchAthleteMetrics={patchAthleteMetrics}
              onSave={async (patch) => { await FS.saveInjury({ ...active, ...patch }); await reload(); }}
              onDelete={() => removeInjury(active.id)} onBack={() => setActiveId(null)} />;
  } else {
    caseContent = (
      <div className="injury-case-surface injury-case-welcome">
        <IjLabel>CASE WORKSPACE</IjLabel>
        <h2>{injuries.length ? '选择一个病例开始工作' : '尚无损伤病例'}</h2>
        <p>{injuries.length
          ? (workflowMode === 'rehab' ? '选择病例后直接进入康复方案，病例列表始终保持可见。' : '病例列表保持可见，切换病例不会离开当前工作台。')
          : '先登记病例；同一记录会贯通损伤筛查、康复处方、评估和人工回归条件。'}</p>
        <button className="btn primary" onClick={() => setCreating(true)}>＋ 登记损伤</button>
      </div>
    );
  }

  return (
    <main className="injury-workbench-page" data-injury-workflow-mode={workflowMode}>
      <div className="injury-workbench-head">
        <button className="btn injury-athlete-back" onClick={onBack}>← 返回运动员</button>
        <div className="injury-athlete-context">
          <span>当前运动员</span>
          <strong>{athlete?.name}</strong>
          <small>{workflowMode === 'rehab' ? '康复处方与实际执行共用该病例档案' : '筛查事实、阶段与评估共用该病例档案'}</small>
        </div>
        <button className="btn primary injury-new-case" onClick={() => { setCreating(true); setActiveId(null); }}>＋ 登记损伤</button>
      </div>
      <div className="injury-workbench">
        <aside className="injury-case-rail" aria-label="病例列表">
          <div className="injury-case-rail-head">
            <div><strong>病例</strong><span>{injuries.length} 个档案</span></div>
            <button className="btn" onClick={() => { setCreating(true); setActiveId(null); }}>新建</button>
          </div>
          <input className="injury-case-search" value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索部位、伤型或阶段" />
          <div className="injury-case-filters">
            {[
              ['all', `全部 ${injuries.length}`],
              ['active', `在案 ${injuries.filter(i => i.status !== 'available').length}`],
              ['available', `可用 ${injuries.filter(i => i.status === 'available').length}`],
            ].map(([id, label]) => <button key={id} className={statusFilter === id ? 'on' : ''} onClick={() => setStatusFilter(id)}>{label}</button>)}
          </div>
          <label className="injury-case-phase-filter">
            <span>阶段筛选</span>
            <select value={phaseFilter} onChange={event => setPhaseFilter(event.target.value)}>
              <option value="all">全部阶段</option>
              {IJ_PHASES.map(phase => <option key={phase.id} value={phase.id}>{phase.label}</option>)}
            </select>
          </label>
          <div className="injury-case-list">
            {visibleInjuries.map(inj => {
              const m = IJ_STATUS_META[inj.status] || IJ_STATUS_META.unavailable;
              return (
                <button key={inj.id} className={activeId === inj.id && !creating ? 'injury-case-row on' : 'injury-case-row'} onClick={() => { setActiveId(inj.id); setCreating(false); }} style={{ '--injury-status-color': m.color }}>
                  <strong>{inj.bodyPart}{inj.side ? ` (${inj.side})` : ''} · {(inj.type || '').split(' ')[0]}</strong>
                  <span>{m.label} · {IJ_PHASE_LABEL[inj.phase]}<br/>{inj.date || '日期未记录'} 建档</span>
                </button>
              );
            })}
            {visibleInjuries.length === 0 && <div className="injury-case-list-empty">{injuries.length ? '没有匹配的病例。' : '暂无病例记录。'}</div>}
          </div>
        </aside>
        <section className="injury-case-main">{caseContent}</section>
      </div>
    </main>
  );
}

// ── InjuryCase (4 subflows) ─────────────────────────────────────────────────
function InjuryCase({ injury, athlete, groups, seasons, exercises, workflowMode = 'screening', patchAthleteMetrics, onSave, onDelete, onBack }) {
  const [tab, setTab] = ijS(workflowMode === 'rehab' ? 'rehab' : 'overview');
  const [pendingPhase, setPendingPhase] = ijS(null);
  const [editCaseRevision, setEditCaseRevision] = ijS(0);
  const [saveState, setSaveState] = ijS('idle');
  const [saveError, setSaveError] = ijS('');
  const m = IJ_STATUS_META[injury.status] || IJ_STATUS_META.unavailable;
  const days = injury.expectedRTP ? ijDaysBetween(ijToday(), injury.expectedRTP) : null;
  const assLSI = (injury.assessments || []).filter(x => x.lsi != null).sort((x, y) => String(x.date).localeCompare(String(y.date)));
  const latestLSI = assLSI.length ? assLSI[assLSI.length - 1].lsi : null;
  const phaseLabel = (IJ_PHASE_LABEL[injury.phase] || injury.phase).split(' ').pop();
  const rehabSummary = window.RehabTrainingModel.summarize(injury.rehab);
  const criteria = Array.isArray(injury.rtpCriteria) ? injury.rtpCriteria : [];
  const metCount = criteria.filter(criterion => criterion.met).length;
  const filesCount = Array.isArray(injury.pathologyReports) ? injury.pathologyReports.length : 0;
  const tabs = [['overview', '当前病例'], ['rehab', '康复安排'], ['progress', '评估与回归'], ['timeline', '时间线'], ['files', '附件']];
  ijE(() => { setTab(workflowMode === 'rehab' ? 'rehab' : 'overview'); }, [workflowMode]);
  const savePatch = async (patch) => {
    setSaveState('saving');
    setSaveError('');
    try {
      await onSave(patch);
      setSaveState('saved');
      return true;
    } catch (error) {
      setSaveState('error');
      setSaveError(error?.message || '保存失败，请重试。');
      return false;
    }
  };
  const confirmPhase = async () => {
    if (!pendingPhase) return;
    const phaseHistory = [
      ...(Array.isArray(injury.phaseHistory) ? injury.phaseHistory : []),
      {
        id: ijUid('ph_'),
        from: injury.phase || null,
        to: pendingPhase,
        date: ijToday(),
        changedAt: new Date().toISOString(),
        source: 'manual',
      },
    ];
    const ok = await savePatch({ phase: pendingPhase, phaseHistory });
    if (ok) setPendingPhase(null);
  };
  const nextTab = workflowMode === 'rehab' ? 'rehab' : 'progress';
  const nextTitle = workflowMode === 'rehab'
    ? (rehabSummary.exercises ? '继续记录康复执行' : '建立首个康复安排')
    : (injury.nextRecheck ? `完成 ${injury.nextRecheck} 复评` : '记录下一次评估');
  const lsiTone = latestLSI == null ? 'muted' : latestLSI >= 90 ? 'positive' : latestLSI >= 75 ? 'warning' : 'negative';
  const currentPhaseIndex = Math.max(0, IJ_PHASES.findIndex(phase => phase.id === injury.phase));

  return (
    <div className="injury-command-layout" data-injury-command-layout data-ai-contract="injury-command-workspace-v1">
      <article className="injury-case-surface injury-case-command">
        <header className="injury-case-command-head">
          <div className="injury-case-title">
            <button className="btn" onClick={onBack}>← 病例列表</button>
            <div>
              <span className="injury-status-label" style={{ '--injury-status-color': m.color }}>{m.label}</span>
              <h2>{injury.bodyPart}{injury.side ? ` (${injury.side})` : ''} · {(injury.type || '').split(' ')[0]}</h2>
              <p>{injury.mechanism || '机制未记录'} · {injury.date || '日期未记录'} 建档</p>
            </div>
          </div>
          <div className="injury-case-actions">
            <span data-injury-save-state={saveState}>
              {saveState === 'saving' ? '保存中…' : saveState === 'saved' ? '已保存' : saveState === 'error' ? '保存失败' : ''}
            </span>
            <button className="btn" onClick={() => { setTab('overview'); setEditCaseRevision(value => value + 1); }}>编辑病例</button>
            <button className="btn injury-delete-case" onClick={onDelete}>删除</button>
          </div>
        </header>

        <nav className="injury-phase-track" aria-label="康复阶段">
          {IJ_PHASES.map((phase, index) => {
            const state = index < currentPhaseIndex ? 'done' : index === currentPhaseIndex ? 'current' : 'future';
            return (
              <button
                key={phase.id}
                className={`${state}${pendingPhase === phase.id ? ' pending' : ''}`}
                aria-current={state === 'current' ? 'step' : undefined}
                onClick={() => phase.id !== injury.phase && setPendingPhase(phase.id)}
              >
                <span>{index < currentPhaseIndex ? '✓' : index + 1}</span>
                {phase.label.split(' ').pop()}
              </button>
            );
          })}
        </nav>

        {pendingPhase && <div className="injury-phase-confirm" data-injury-phase-confirm>
          <span>将阶段变更为 <b>{IJ_PHASE_LABEL[pendingPhase]}</b>。系统只记录人工决定，不自动推进阶段。</span>
          <button className="btn" onClick={() => setPendingPhase(null)}>取消</button>
          <button className="btn primary" disabled={saveState === 'saving'} onClick={confirmPhase}>确认变更阶段</button>
        </div>}
        {saveError && <div role="alert" className="injury-inline-error">{saveError}</div>}

        <section className="injury-case-kpis" aria-label="当前病例概览">
          <article><span>当前阶段</span><strong>{phaseLabel}</strong><small>人工确认</small></article>
          <article><span>最近 LSI</span><strong className={lsiTone}>{latestLSI != null ? `${latestLSI}%` : '暂无'}</strong><small>{assLSI.length ? `${assLSI.length} 条评估` : '等待评估'}</small></article>
          <article><span>下次复评</span><strong>{injury.nextRecheck || '未安排'}</strong><small>{injury.nextRecheck ? '病例计划' : '需要设置'}</small></article>
          <article><span>预计回归</span><strong>{days == null ? '未设置' : `${Math.max(0, days)} 天`}</strong><small>仅日期参考</small></article>
        </section>

        <nav className="injury-case-tabs" aria-label="病例工作区">
          {tabs.map(([id, label]) => (
            <button key={id} className={tab === id ? 'on' : ''} aria-current={tab === id ? 'page' : undefined} onClick={() => setTab(id)}>{label}</button>
          ))}
        </nav>
        <div className="injury-case-tab-content">
          {tab === 'overview' && <OverviewTab injury={injury} editSignal={editCaseRevision} onSave={savePatch} />}
          {tab === 'rehab' && <RehabTab injury={injury} exercises={exercises} onSave={savePatch} />}
          {tab === 'progress' && <ProgressTab injury={injury} onSave={savePatch} />}
          {tab === 'timeline' && <InjuryTimelineTab injury={injury} />}
          {tab === 'files' && <InjuryFilesTab injury={injury} onSave={savePatch} />}
        </div>
      </article>

      <aside className="injury-context-rail" aria-label="病例上下文">
        <section className="injury-context-next">
          <span>下一步</span>
          <strong>{nextTitle}</strong>
          <p>{workflowMode === 'rehab' ? '处方与实际完成分别记录。' : '补充患侧、健侧和人工回归条件。'}</p>
          <button className="btn primary" onClick={() => setTab(nextTab)}>{workflowMode === 'rehab' ? '打开康复安排' : '记录评估'}</button>
        </section>
        <section>
          <header><strong>证据与来源</strong><button onClick={() => setTab('timeline')}>查看时间线</button></header>
          <button className="injury-context-row" onClick={() => setTab('progress')}>
            <span>最近评估</span><strong>{latestLSI != null ? `LSI ${latestLSI}%` : '暂无评估'}</strong>
          </button>
          <button className="injury-context-row" onClick={() => setTab('rehab')}>
            <span>康复执行</span><strong>{rehabSummary.completed}/{rehabSummary.exercises} 动作</strong>
          </button>
          <button className="injury-context-row" onClick={() => setTab('files')}>
            <span>病例附件</span><strong>{filesCount} 个</strong>
          </button>
        </section>
        <section>
          <header><strong>人工回归条件</strong><button onClick={() => setTab('progress')}>管理</button></header>
          {criteria.length === 0 ? <p className="injury-context-empty">尚未建立回归条件。</p> : criteria.slice(0, 5).map(criterion => (
            <div className="injury-context-criterion" key={criterion.id}>
              <span className={criterion.met ? 'met' : ''}>{criterion.met ? '✓' : ''}</span>
              <b>{criterion.label || '未命名条件'}</b>
            </div>
          ))}
          {criteria.length > 0 && <small>{metCount}/{criteria.length} 已人工确认</small>}
        </section>
        <section className="injury-system-boundary">
          <strong>系统边界</strong>
          <p>保存事实、处方、执行和人工决定，不自动生成医学诊断或回归资格。</p>
        </section>
      </aside>
    </div>
  );
}

function InjuryTimelineTab({ injury }) {
  const events = ijM(() => {
    const rows = [];
    if (injury.date) rows.push({
      id: `case-${injury.id}`,
      date: injury.date,
      kind: 'case',
      title: '损伤病例登记',
      detail: `${injury.bodyPart || '部位未记录'} · ${injury.type || '伤型未记录'}`,
      source: '病例事实',
    });
    (Array.isArray(injury.phaseHistory) ? injury.phaseHistory : []).forEach(event => rows.push({
      id: event.id || `phase-${event.changedAt}`,
      date: event.date || String(event.changedAt || '').slice(0, 10),
      kind: 'phase',
      title: `阶段变更为 ${(IJ_PHASE_LABEL[event.to] || event.to || '').split(' ').pop()}`,
      detail: `由 ${(IJ_PHASE_LABEL[event.from] || event.from || '未记录').split(' ').pop()} 人工推进`,
      source: '人工确认',
    }));
    (Array.isArray(injury.assessments) ? injury.assessments : []).forEach(assessment => rows.push({
      id: assessment.id,
      date: assessment.date,
      kind: 'assessment',
      title: assessment.name || '评估记录',
      detail: assessment.lsi == null ? '患侧与健侧结果已记录' : `LSI ${assessment.lsi}%`,
      source: '评估',
    }));
    (Array.isArray(injury.rehab) ? injury.rehab : []).forEach(day => {
      const items = Array.isArray(day.items) ? day.items : [];
      const completed = items.filter(item => item.status === 'completed' || item.done).length;
      rows.push({
        id: day.id,
        date: day.date,
        kind: 'rehab',
        title: day.focus || '康复训练',
        detail: `${completed}/${items.length} 个动作已记录执行`,
        source: '康复训练',
      });
    });
    (Array.isArray(injury.pathologyReports) ? injury.pathologyReports : []).forEach((report, index) => rows.push({
      id: `file-${report.addedAt || index}`,
      date: report.addedAt ? new Date(report.addedAt).toISOString().slice(0, 10) : '',
      kind: 'file',
      title: report.name || '病例附件',
      detail: '附件加入病例',
      source: '附件',
    }));
    return rows
      .filter(row => row.date)
      .sort((a, b) => String(b.date).localeCompare(String(a.date)));
  }, [injury]);

  return (
    <section className="injury-timeline-workflow" data-injury-timeline data-ai-contract="injury-case-timeline-v1">
      <header className="injury-panel-head">
        <div><h3>病例时间线</h3><p>将病例事实、阶段决定、评估、康复执行和附件按日期汇总。</p></div>
      </header>
      <div className="injury-dossier-summary">
        <div><span>病例 ID</span><strong>{injury.id}</strong></div>
        <div><span>负责人</span><strong>{injury.owner || '未设置'}</strong></div>
        <div><span>病例状态</span><strong>{(IJ_STATUS_META[injury.status] || IJ_STATUS_META.unavailable).label}</strong></div>
        <div><span>记录数量</span><strong>{events.length}</strong></div>
      </div>
      {events.length === 0 ? <div className="injury-subflow-empty">当前病例还没有可排序的时间记录。</div> : (
        <div className="injury-timeline-list">
          {events.map(event => (
            <article key={event.id} data-record-kind={event.kind}>
              <time>{event.date}</time>
              <div><strong>{event.title}</strong><p>{event.detail}</p></div>
              <span>{event.source}</span>
            </article>
          ))}
        </div>
      )}
    </section>
  );
}

// ── OverviewTab - view(对照 mockup 受伤部位 + 康复进程)/ edit(登记表单)切换 ──
function OverviewTab({ injury, isNew, editSignal = 0, onSave, onCancel }) {
  const [f, setF] = ijS(() => ({ bodyPart: '', side: null, type: IJ_TYPES[0], mechanism: 'non-contact', date: ijToday(), severity: '', phase: 'acute', status: 'unavailable', expectedRTP: '', owner: '', notes: '', painVAS: '', swelling: '', rom: '', imaging: '', phaseGoal: '', nextRecheck: '', pathologyReports: [], ...injury }));
  const [mode, setMode] = ijS(isNew ? 'edit' : 'view');
  const [formSaveState, setFormSaveState] = ijS('idle');
  const [formSaveError, setFormSaveError] = ijS('');
  const [fileError, setFileError] = ijS('');
  const upd = (k) => (e) => setF(s => ({ ...s, [k]: e.target.value }));
  ijE(() => { if (!isNew && editSignal > 0) setMode('edit'); }, [editSignal, isNew]);

  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setFileError('');
    if (file.size > 8 * 1024 * 1024) { setFileError('文件超过 8MB，请压缩后再上传。'); e.target.value = ''; return; }
    const reader = new FileReader();
    reader.onload = () => setF(s => ({ ...s, pathologyReports: [...(s.pathologyReports || []), { name: file.name, dataUrl: reader.result, addedAt: Date.now() }] }));
    reader.readAsDataURL(file);
    e.target.value = '';
  };
  const removeReport = (i) => setF(s => ({ ...s, pathologyReports: s.pathologyReports.filter((_, j) => j !== i) }));
  const save = async () => {
    if (!f.bodyPart) { setFormSaveError('请先在身体图上选择受伤部位。'); return; }
    setFormSaveState('saving');
    setFormSaveError('');
    try {
      const ok = await onSave(f);
      if (ok === false) {
        setFormSaveState('error');
        setFormSaveError('保存失败，请检查上方提示后重试。');
        return;
      }
      setFormSaveState('saved');
      if (!isNew) setMode('view');
    } catch (error) {
      setFormSaveState('error');
      setFormSaveError(error?.message || '保存失败，请重试。');
    }
  };

  if (mode === 'view') {
    const mechLabel = (IJ_MECHANISMS.find(([v]) => v === f.mechanism) || [])[1] || f.mechanism || '未记录';
    const ass = (injury.assessments || []).filter(a => a.lsi != null).sort((a, b) => String(a.date).localeCompare(String(b.date)));
    const last = ass.length ? ass[ass.length - 1] : null;
    const lsi = last ? last.lsi : null;
    const lsiColor = lsi == null ? 'var(--muted)' : lsi >= 90 ? 'var(--pos)' : lsi >= 75 ? 'var(--warn)' : 'var(--neg)';
    const R = 34, CC = 2 * Math.PI * R;
    const cardStyle = { background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 12, padding: 16, minWidth: 0 };
    const Row = ({ k, v }) => (
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, padding: '7px 0', borderBottom: '1px solid var(--hairline)' }}>
        <span style={{ fontSize: 12, color: 'var(--muted)' }}>{k}</span>
        <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--text)', textAlign: 'right' }}>{(v === '' || v == null) ? '未记录' : v}</span>
      </div>
    );
    return (
      <div className="injury-overview-grid" style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)', gap: 16, alignItems: 'start' }}>
        {/* 受伤部位 */}
        <div style={cardStyle}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }}>
            <span style={{ fontSize: 13.5, fontWeight: 600 }}>受伤部位</span>
            <button className="btn" style={{ fontSize: 11, padding: '3px 9px' }} onClick={() => setMode('edit')}>✎ 编辑</button>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '140px 1fr', gap: 16, alignItems: 'center' }}>
            <div style={{ textAlign: 'center' }}><BodyMap value={{ part: f.bodyPart, side: f.side }} onPick={() => {}}/></div>
            <div>
              {f.bodyPart && <span style={{ display: 'inline-block', fontSize: 11.5, fontWeight: 600, color: 'var(--neg)', background: 'rgba(220,38,38,.10)', padding: '2px 9px', borderRadius: 999, marginBottom: 8 }}>{f.bodyPart}{f.side ? ` · ${f.side}` : ''}</span>}
              <Row k="疼痛 VAS" v={f.painVAS !== '' && f.painVAS != null ? `${f.painVAS} / 10` : '未记录'}/>
              <Row k="肿胀" v={f.swelling}/>
              <Row k="ROM" v={f.rom}/>
            </div>
          </div>
        </div>
        {/* 康复进程 */}
        <div style={cardStyle}>
          <div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: 10 }}>康复进程</div>
          <Row k="机制" v={mechLabel}/>
          <Row k="影像" v={f.imaging}/>
          <Row k="本阶段目标" v={f.phaseGoal}/>
          <Row k="下次复评" v={f.nextRecheck}/>
          <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginTop: 14 }}>
            <svg width="84" height="84" viewBox="0 0 84 84" style={{ flexShrink: 0 }}>
              <circle cx="42" cy="42" r={R} fill="none" stroke="var(--panel-hi)" strokeWidth="9"/>
              {lsi != null && <circle cx="42" cy="42" r={R} fill="none" stroke={lsiColor} strokeWidth="9" strokeLinecap="round" strokeDasharray={CC} strokeDashoffset={CC * (1 - Math.max(0, Math.min(100, lsi)) / 100)} transform="rotate(-90 42 42)"/>}
              <text x="42" y="40" textAnchor="middle" style={{ fontFamily: 'var(--font-mono)', fontSize: 18, fontWeight: 700, fill: lsiColor }}>{lsi != null ? lsi : '-'}</text>
              <text x="42" y="55" textAnchor="middle" style={{ fontSize: 9, fill: 'var(--muted-2)' }}>LSI%</text>
            </svg>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 11.5, color: 'var(--muted)', marginBottom: 6 }}>双侧测力台 · 等长峰力(健/患)</div>
              {last ? (<>
                {[['健侧', last.healthy, 'var(--pos)'], ['患侧', last.injured, 'var(--warn)']].map(([lab, val, col]) => {
                  const mx = Math.max(last.healthy || 0, last.injured || 0, 1);
                  return (
                    <div key={lab} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
                      <span style={{ width: 28, fontSize: 11, color: 'var(--muted)' }}>{lab}</span>
                      <span style={{ flex: 1, height: 8, background: 'var(--panel-hi)', borderRadius: 5, overflow: 'hidden' }}><i style={{ display: 'block', height: '100%', width: `${(val / mx) * 100}%`, background: col, borderRadius: 5 }}/></span>
                      <span className="mono" style={{ fontSize: 11.5, color: 'var(--text-2)', width: 44, textAlign: 'right' }}>{val}N</span>
                    </div>
                  );
                })}
                <div style={{ fontSize: 10.5, color: 'var(--muted-2)', marginTop: 4 }}>LSI 为人工记录的参考指标；是否推进阶段由专业人员综合判断。</div>
              </>) : <div style={{ fontSize: 11.5, color: 'var(--muted-2)' }}>暂无双侧测力评估 · 到「进展 · LSI」录入</div>}
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ── edit mode (registry form) ──
  return (
    <div className="injury-edit-grid" data-injury-case-editor style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: 20, alignItems: 'start' }}>
      <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: 10, textAlign: 'center' }}>
        <IjLabel style={{ marginBottom: 6 }}>Body map 部位</IjLabel>
        <BodyMap value={{ part: f.bodyPart, side: f.side }} onPick={({ part, side }) => setF(s => ({ ...s, bodyPart: part, side }))}/>
        <div style={{ fontSize: 11, color: 'var(--text-2)', marginTop: 6, fontWeight: 600 }}>{f.bodyPart ? `${f.bodyPart}${f.side ? ` (${f.side})` : ''}` : '点击选择部位'}</div>
      </div>
      <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '14px 16px', display: 'flex', flexDirection: 'column', gap: 12 }}>
        <div className="injury-form-grid-3" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
          <label><IjLabel>Type 伤型</IjLabel><select value={f.type} onChange={upd('type')} style={ijInput}>{IJ_TYPES.map(t => <option key={t}>{t}</option>)}</select></label>
          <label><IjLabel>Mechanism 机制</IjLabel><select value={f.mechanism} onChange={upd('mechanism')} style={ijInput}>{IJ_MECHANISMS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select></label>
          <label><IjLabel>Severity 分级</IjLabel><input value={f.severity} onChange={upd('severity')} placeholder="e.g. Grade II" style={ijInput}/></label>
        </div>
        <div className="injury-form-grid-3" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
          <label><IjLabel>Injury date 受伤日期</IjLabel><input type="date" value={f.date} onChange={upd('date')} style={ijInput}/></label>
          <label><IjLabel>Phase 阶段</IjLabel><select value={f.phase} onChange={upd('phase')} style={ijInput}>{IJ_PHASES.map(p => <option key={p.id} value={p.id}>{p.label}</option>)}</select></label>
          <label><IjLabel>Status 状态</IjLabel><select value={f.status} onChange={upd('status')} style={ijInput}>{Object.entries(IJ_STATUS_META).map(([v, mm]) => <option key={v} value={v}>{mm.label}</option>)}</select></label>
        </div>
        <div className="injury-form-grid-2" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <label><IjLabel>Expected RTP 预计回归</IjLabel><input type="date" value={f.expectedRTP} onChange={upd('expectedRTP')} style={ijInput}/></label>
          <label><IjLabel>Owner 负责人</IjLabel><input value={f.owner} onChange={upd('owner')} placeholder="e.g. Team physio / S&C" style={ijInput}/></label>
        </div>
        {/* 临床体征(对照 mockup 受伤部位 + 康复进程) */}
        <div className="injury-form-grid-3" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
          <label><IjLabel>疼痛 VAS 0-10</IjLabel><input type="number" min="0" max="10" value={f.painVAS} onChange={upd('painVAS')} style={ijInput}/></label>
          <label><IjLabel>肿胀</IjLabel><input value={f.swelling} onChange={upd('swelling')} placeholder="如 轻微 / 中度" style={ijInput}/></label>
          <label><IjLabel>ROM</IjLabel><input value={f.rom} onChange={upd('rom')} placeholder="如 背屈 -8°" style={ijInput}/></label>
        </div>
        <div className="injury-form-grid-3" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 10 }}>
          <label><IjLabel>影像</IjLabel><input value={f.imaging} onChange={upd('imaging')} placeholder="如 X线阴性" style={ijInput}/></label>
          <label><IjLabel>本阶段目标</IjLabel><input value={f.phaseGoal} onChange={upd('phaseGoal')} placeholder="如 恢复背屈ROM" style={ijInput}/></label>
          <label><IjLabel>下次复评</IjLabel><input type="date" value={f.nextRecheck} onChange={upd('nextRecheck')} style={ijInput}/></label>
        </div>
        <label><IjLabel>Notes 备注</IjLabel><textarea value={f.notes} onChange={upd('notes')} rows={2} style={{ ...ijInput, resize: 'vertical', lineHeight: 1.5 }}/></label>
        <div>
          <IjLabel style={{ marginBottom: 6 }}>Pathology reports 病理报告（医院诊断 · PDF/图片）</IjLabel>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 6 }}>
            {(f.pathologyReports || []).map((r, i) => (
              <span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11.5, padding: '4px 8px', borderRadius: 6, background: 'var(--panel-hi)', border: '1px solid var(--border)' }}>
                <a href={r.dataUrl || r.url} target="_blank" rel="noreferrer" style={{ color: 'var(--accent)', textDecoration: 'none' }}>📄 {r.name}</a>
                <button onClick={() => removeReport(i)} style={{ background: 'none', border: 0, color: 'var(--muted-2)', cursor: 'pointer', fontSize: 12 }}>×</button>
              </span>
            ))}
          </div>
          <label className="btn" style={{ fontSize: 11, cursor: 'pointer', display: 'inline-block' }}>
            + 上传报告 Upload<input type="file" accept=".pdf,image/*" onChange={onFile} style={{ display: 'none' }}/>
          </label>
          {fileError && <div role="alert" className="injury-inline-error">{fileError}</div>}
        </div>
        {formSaveError && <div role="alert" data-injury-form-save-error style={{ padding: '8px 10px', borderRadius: 6, color: 'var(--neg)', background: 'color-mix(in srgb, var(--neg) 8%, transparent)', fontSize: 11.5 }}>{formSaveError}</div>}
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 4, alignItems: 'center' }}>
          {formSaveState === 'saving' && <span style={{ fontSize: 11, color: 'var(--muted)' }}>保存中…</span>}
          {isNew && <button className="btn" onClick={onCancel}>Cancel</button>}
          {!isNew && <button className="btn" onClick={() => setMode('view')}>取消</button>}
          <button className="btn primary" disabled={formSaveState === 'saving'} onClick={save}>{isNew ? 'Create injury 创建档案' : 'Save 保存'}</button>
        </div>
      </div>
    </div>
  );
}

// ── Attachments - same case record, with explicit size and save feedback ─────
function InjuryFilesTab({ injury, onSave }) {
  const [reports, setReports] = ijS(() => injury.pathologyReports || []);
  const [state, setState] = ijS('idle');
  const [error, setError] = ijS('');
  const persist = async (next) => {
    setReports(next);
    setState('saving');
    setError('');
    try {
      const ok = await onSave({ pathologyReports: next });
      if (ok === false) throw new Error('保存失败，请重试。');
      setState('saved');
    } catch (e) {
      setState('error');
      setError(e?.message || '附件保存失败，请重试。');
    }
  };
  const onFile = (e) => {
    const file = e.target.files && e.target.files[0];
    if (!file) return;
    setError('');
    if (file.size > 8 * 1024 * 1024) {
      setError('文件超过 8MB，请压缩后再上传。');
      e.target.value = '';
      return;
    }
    const reader = new FileReader();
    reader.onload = () => persist([...reports, { name: file.name, dataUrl: reader.result, addedAt: Date.now() }]);
    reader.onerror = () => setError('文件读取失败，请重新选择。');
    reader.readAsDataURL(file);
    e.target.value = '';
  };
  return (
    <section className="injury-files-panel" data-injury-files-panel>
      <div className="injury-panel-head">
        <div>
          <h3>病理、影像与诊断附件</h3>
          <p>附件写入当前病例；单个文件上限 8MB，支持 PDF 与图片。</p>
        </div>
        <label className="btn primary">＋ 上传附件<input type="file" accept=".pdf,image/*" onChange={onFile} style={{ display: 'none' }}/></label>
      </div>
      {state === 'saving' && <div className="injury-save-note">正在保存附件…</div>}
      {state === 'saved' && <div className="injury-save-note ok">附件已保存 ✓</div>}
      {error && <div role="alert" className="injury-inline-error">{error}</div>}
      {reports.length === 0 ? <div className="injury-subflow-empty">当前病例暂无附件。</div> : (
        <div className="injury-file-list">
          {reports.map((report, index) => (
            <article key={`${report.name}-${report.addedAt || index}`}>
              <div className="injury-file-icon">PDF</div>
              <div><strong>{report.name}</strong><span>{report.addedAt ? new Date(report.addedAt).toLocaleString() : '既有病例附件'}</span></div>
              <a className="btn" href={report.dataUrl || report.url} target="_blank" rel="noreferrer">查看</a>
              <button className="btn" onClick={() => persist(reports.filter((_, i) => i !== index))}>移除</button>
            </article>
          ))}
        </div>
      )}
    </section>
  );
}

function RehabField({ label, field, value, onChange, placeholder, type = 'text', min, max, step }) {
  return (
    <label className="injury-rehab-field">
      <span>{label}</span>
      <input
        data-rehab-field={field}
        type={type}
        min={min}
        max={max}
        step={step}
        value={value}
        onChange={event => onChange(event.target.value)}
        placeholder={placeholder}
      />
    </label>
  );
}

// ── RehabTab (structured prescription + execution, one shared case record) ──
function RehabTab({ injury, exercises, onSave }) {
  const model = window.RehabTrainingModel;
  const [rehab, setRehab] = ijS(() => model.normalizeDays(injury.rehab));
  const [pickerDay, setPickerDay] = ijS(null); // which day's exercise picker is open (index | null)
  const [expandedItemId, setExpandedItemId] = ijS(null);
  const [saveState, setSaveState] = ijS('idle');
  const [saveError, setSaveError] = ijS('');
  const persist = async (next) => {
    const normalized = model.normalizeDays(next);
    setRehab(normalized);
    setSaveState('saving');
    setSaveError('');
    try {
      const ok = await onSave({ rehab: normalized });
      if (ok === false) throw new Error('保存失败，请重试。');
      setSaveState('saved');
    } catch (error) {
      setSaveState('error');
      setSaveError(error?.message || '康复方案保存失败，请重试。');
    }
  };

  const addDay = () => persist([...rehab, { id: ijUid('rd_'), date: ijToday(), focus: '', note: '', items: [] }]);
  const removeDay = (di) => persist(rehab.filter((_, i) => i !== di));
  const setDayField = (di, key, value) => persist(rehab.map((d, i) => i === di ? { ...d, [key]: value } : d));
  const addItem = (di, exId) => {
    const ex = exercises.find(e => e.id === exId); if (!ex) return;
    const item = model.createItem({ id: ijUid('ri_'), exercise: ex });
    persist(rehab.map((d, i) => i === di ? { ...d, items: [...d.items, item] } : d));
  };
  const updItem = (di, ii, patch) => persist(rehab.map((d, i) => i === di ? { ...d, items: d.items.map((it, j) => j === ii ? { ...it, ...patch } : it) } : d));
  const updItemField = (di, ii, section, key, value) => persist(rehab.map((d, i) => i === di ? {
    ...d,
    items: d.items.map((it, j) => j === ii ? {
      ...it,
      [section]: { ...it[section], [key]: value },
    } : it),
  } : d));
  const toggleItem = (di, ii, item) => {
    const completing = item.status !== 'completed';
    updItem(di, ii, {
      status: completing ? 'completed' : 'planned',
      completedAt: completing ? new Date().toISOString() : null,
    });
  };
  const removeItem = (di, ii) => persist(rehab.map((d, i) => i === di ? { ...d, items: d.items.filter((_, j) => j !== ii) } : d));
  const summary = model.summarize(rehab);
  const prescriptionSummary = (item) => [
    item.prescription.sets && `${item.prescription.sets} 组`,
    item.prescription.reps && `${item.prescription.reps}`,
    item.prescription.load && `${item.prescription.load}${item.prescription.loadUnit || 'kg'}`,
    item.prescription.rpe && `RPE ${item.prescription.rpe}`,
    item.prescription.tempo && `Tempo ${item.prescription.tempo}`,
  ].filter(Boolean).join(' · ') || '尚未填写处方';

  return (
    <div className="injury-rehab-workflow" data-injury-rehab-workflow data-ai-contract="rehab-training-v1">
      <div className="injury-panel-head">
        <div>
          <IjLabel>REHAB TRAINING</IjLabel>
          <h3>康复训练安排</h3>
          <p>处方目标与现场执行分开记录，字段与训练工作流保持一致。</p>
        </div>
        <button className="btn primary" onClick={addDay}>＋ 添加康复日</button>
      </div>
      <div className="injury-rehab-summary">
        <span><b>{summary.days}</b> 康复日</span>
        <span><b>{summary.exercises}</b> 个动作</span>
        <span><b>{summary.completed}</b> 已完成</span>
        <em>{saveState === 'saving' ? '保存中…' : saveState === 'saved' ? '已保存 ✓' : ''}</em>
      </div>
      {saveError && <div role="alert" className="injury-inline-error">{saveError}</div>}
      {rehab.length === 0 && <div className="injury-subflow-empty">还没有康复安排。添加康复日后，可从动作库建立当日处方。</div>}
      {rehab.map((day, di) => (
        <div key={day.id} className="injury-rehab-day">
          <div className="injury-rehab-day-head">
            <label>
              <span>训练日期</span>
              <input type="date" value={day.date} onChange={e => setDayField(di, 'date', e.target.value)} />
            </label>
            <label className="injury-rehab-day-focus">
              <span>当日重点</span>
              <input data-rehab-day-field="focus" value={day.focus} onChange={e => setDayField(di, 'focus', e.target.value)} placeholder="例如：踝背屈活动度与耐受性" />
            </label>
            <span className="injury-rehab-day-progress">{day.items.filter(i => i.status === 'completed').length}/{day.items.length} 完成</span>
            <button className="btn injury-rehab-day-remove" onClick={() => removeDay(di)}>移除康复日</button>
          </div>
          {day.items.map((it, ii) => (
            <article key={it.id} className={it.status === 'completed' ? 'injury-rehab-item is-complete' : 'injury-rehab-item'} data-rehab-exercise-id={it.exerciseId}>
              <header className="injury-rehab-item-head">
                <button
                  className="injury-rehab-complete"
                  aria-label={it.status === 'completed' ? '标记为未完成' : '标记为已完成'}
                  onClick={() => toggleItem(di, ii, it)}
                >{it.status === 'completed' ? '✓' : '○'}</button>
                <div>
                  <strong>{it.name}</strong>
                  <span>{prescriptionSummary(it)}</span>
                </div>
                <span className={it.status === 'completed' ? 'injury-rehab-item-state complete' : 'injury-rehab-item-state'}>
                  {it.status === 'completed' ? '执行已记录' : '等待执行'}
                </span>
                <button className="btn injury-rehab-edit" onClick={() => setExpandedItemId(expandedItemId === it.id ? null : it.id)}>
                  {expandedItemId === it.id ? '收起' : '编辑'}
                </button>
                <button className="btn injury-rehab-remove" onClick={() => removeItem(di, ii)}>移除动作</button>
              </header>

              {expandedItemId === it.id && <div className="injury-rehab-item-editor">
              <section className="injury-rehab-section" data-rehab-prescription>
                <div className="injury-rehab-section-title"><strong>处方目标</strong><span>教练计划</span></div>
                <div className="injury-rehab-field-grid prescription">
                  <RehabField label="组数" field="sets" value={it.prescription.sets} onChange={value => updItemField(di, ii, 'prescription', 'sets', value)} placeholder="4" type="number" min="0" />
                  <RehabField label="次数 / 时长" field="reps" value={it.prescription.reps} onChange={value => updItemField(di, ii, 'prescription', 'reps', value)} placeholder="8 或 30s" />
                  <RehabField label="负荷 (kg)" field="load" value={it.prescription.load} onChange={value => updItemField(di, ii, 'prescription', 'load', value)} placeholder="20" type="number" min="0" step="0.5" />
                  <RehabField label="目标 RPE" field="rpe" value={it.prescription.rpe} onChange={value => updItemField(di, ii, 'prescription', 'rpe', value)} placeholder="6" type="number" min="0" max="10" step="0.5" />
                  <RehabField label="目标 RIR" field="rir" value={it.prescription.rir} onChange={value => updItemField(di, ii, 'prescription', 'rir', value)} placeholder="3" type="number" min="0" />
                </div>
              </section>

              <section className="injury-rehab-section execution" data-rehab-execution>
                <div className="injury-rehab-section-title"><strong>现场执行</strong><span>实际完成</span></div>
                <div className="injury-rehab-field-grid execution">
                  <RehabField label="完成组数" field="completedSets" value={it.execution.completedSets} onChange={value => updItemField(di, ii, 'execution', 'completedSets', value)} placeholder="4" type="number" min="0" />
                  <RehabField label="完成次数" field="completedReps" value={it.execution.completedReps} onChange={value => updItemField(di, ii, 'execution', 'completedReps', value)} placeholder="8" />
                  <RehabField label="实际负荷 (kg)" field="executionLoad" value={it.execution.load} onChange={value => updItemField(di, ii, 'execution', 'load', value)} placeholder="20" type="number" min="0" step="0.5" />
                  <RehabField label="实际 RPE" field="executionRpe" value={it.execution.rpe} onChange={value => updItemField(di, ii, 'execution', 'rpe', value)} placeholder="6" type="number" min="0" max="10" step="0.5" />
                  <RehabField label="训练前疼痛" field="painBefore" value={it.execution.painBefore} onChange={value => updItemField(di, ii, 'execution', 'painBefore', value)} placeholder="0" type="number" min="0" max="10" />
                  <RehabField label="训练后疼痛" field="painAfter" value={it.execution.painAfter} onChange={value => updItemField(di, ii, 'execution', 'painAfter', value)} placeholder="0" type="number" min="0" max="10" />
                </div>
              </section>

              <details className="injury-rehab-advanced">
                <summary>Tempo、休息、VBT 与备注</summary>
                <div className="injury-rehab-field-grid advanced">
                  <RehabField label="Tempo" field="tempo" value={it.prescription.tempo} onChange={value => updItemField(di, ii, 'prescription', 'tempo', value)} placeholder="3-1-X-0" />
                  <RehabField label="组间休息 (s)" field="restSec" value={it.prescription.restSec} onChange={value => updItemField(di, ii, 'prescription', 'restSec', value)} placeholder="90" type="number" min="0" />
                  <RehabField label="目标速度 (m/s)" field="targetVelocityMS" value={it.prescription.targetVelocityMS} onChange={value => updItemField(di, ii, 'prescription', 'targetVelocityMS', value)} placeholder="0.65" type="number" min="0" step="0.01" />
                  <RehabField label="速度损失上限 (%)" field="velocityLossPct" value={it.prescription.velocityLossPct} onChange={value => updItemField(di, ii, 'prescription', 'velocityLossPct', value)} placeholder="20" type="number" min="0" max="100" />
                  <RehabField label="实测平均速度 (m/s)" field="meanVelocityMS" value={it.execution.meanVelocityMS} onChange={value => updItemField(di, ii, 'execution', 'meanVelocityMS', value)} placeholder="0.62" type="number" min="0" step="0.01" />
                  <RehabField label="外部组 ID" field="externalSetId" value={it.execution.externalSetId} onChange={value => updItemField(di, ii, 'execution', 'externalSetId', value)} placeholder="可选设备记录 ID" />
                </div>
                <label className="injury-rehab-note">
                  <span>处方备注</span>
                  <textarea data-rehab-field="prescriptionNote" value={it.prescription.note} onChange={e => updItemField(di, ii, 'prescription', 'note', e.target.value)} placeholder="技术提示、允许范围或替代动作" />
                </label>
                <label className="injury-rehab-note">
                  <span>执行备注</span>
                  <textarea data-rehab-field="executionNote" value={it.execution.note} onChange={e => updItemField(di, ii, 'execution', 'note', e.target.value)} placeholder="完成质量、症状变化或现场调整" />
                </label>
              </details>
              </div>}
            </article>
          ))}
          <button type="button" className="btn injury-rehab-add" onClick={() => setPickerDay(di)}>＋ 从动作库添加</button>
        </div>
      ))}
      {pickerDay != null && window.ExercisePicker && (() => {
        const Picker = window.ExercisePicker;
        return <Picker exercises={exercises} onPick={(id) => { addItem(pickerDay, id); setPickerDay(null); }} onClose={() => setPickerDay(null)} />;
      })()}
    </div>
  );
}

// ── ProgressTab (assessments → LSI, milestones, expected-date reference) ─────
function ProgressTab({ injury, onSave }) {
  const FS = window.FieldDataStore;
  const [assessments, setAssessments] = ijS(() => injury.assessments || []);
  const [criteria, setCriteria] = ijS(() => injury.rtpCriteria || []);
  const [draft, setDraft] = ijS({ date: ijToday(), name: '', injured: '', healthy: '' });
  const [draftError, setDraftError] = ijS('');

  const persistA = (next) => { setAssessments(next); onSave({ assessments: next }); };
  const persistC = (next) => { setCriteria(next); onSave({ rtpCriteria: next }); };

  const addAssessment = () => {
    if (!draft.name.trim()) { setDraftError('请填写评估项名称。'); return; }
    if (draft.injured === '' || draft.healthy === '') { setDraftError('请同时填写患侧与健侧数值。'); return; }
    const lsi = FS.computeLSI(draft.injured, draft.healthy);
    if (lsi == null || !isFinite(lsi)) { setDraftError('患侧与健侧必须是可计算的数值，且健侧不能为 0。'); return; }
    persistA([...assessments, { id: ijUid('as_'), date: draft.date, name: draft.name, injured: draft.injured, healthy: draft.healthy, lsi }]);
    setDraftError('');
    setDraft({ date: ijToday(), name: '', injured: '', healthy: '' });
  };
  const removeAssessment = (i) => persistA(assessments.filter((_, j) => j !== i));

  const addCriterion = () => persistC([...criteria, { id: ijUid('rc_'), label: '', met: false }]);
  const updCriterion = (i, patch) => persistC(criteria.map((c, j) => j === i ? { ...c, ...patch } : c));
  const removeCriterion = (i) => persistC(criteria.filter((_, j) => j !== i));

  // latest LSI + trend
  const sorted = [...assessments].filter(a => a.lsi != null).sort((a, b) => String(a.date).localeCompare(String(b.date)));
  const latestLSI = sorted.length ? sorted[sorted.length - 1].lsi : null;
  const prevLSI = sorted.length > 1 ? sorted[sorted.length - 2].lsi : null;
  const lsiTrend = (latestLSI != null && prevLSI != null) ? latestLSI - prevLSI : null;
  const days = injury.expectedRTP ? ijDaysBetween(ijToday(), injury.expectedRTP) : null;
  const metCount = criteria.filter(c => c.met).length;

  return (
    <div className="injury-progress-workflow" data-injury-progress-workflow>
      {/* Human-recorded progress summary; no automatic return-to-play decision. */}
      <div className="injury-progress-summary" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 10 }}>
        <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
          <IjLabel>Latest LSI 肢体对称</IjLabel>
          <div style={{ fontSize: 26, fontWeight: 700, fontFamily: 'var(--font-mono)', color: latestLSI == null ? 'var(--muted)' : latestLSI >= 90 ? 'var(--pos)' : latestLSI >= 75 ? 'var(--warn)' : 'var(--neg)' }}>
            {latestLSI != null ? latestLSI + '%' : '暂无'}
            {lsiTrend != null && <span style={{ fontSize: 12, marginLeft: 6, color: lsiTrend >= 0 ? 'var(--pos)' : 'var(--neg)' }}>{lsiTrend >= 0 ? '↗' : '↘'}{Math.abs(Math.round(lsiTrend * 10) / 10)}</span>}
          </div>
          <div style={{ fontSize: 10, color: 'var(--muted-2)' }}>参考值，不自动判定阶段或回归</div>
        </div>
        <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
          <IjLabel>Phase 阶段</IjLabel>
          <div style={{ fontSize: 16, fontWeight: 600, marginTop: 4 }}>{IJ_PHASE_LABEL[injury.phase] || injury.phase}</div>
        </div>
        <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
          <IjLabel>预计日期差</IjLabel>
          <div style={{ fontSize: 26, fontWeight: 700, fontFamily: 'var(--font-mono)', color: days == null ? 'var(--muted)' : days < 0 ? 'var(--neg)' : 'var(--text)' }}>{days != null ? (days >= 0 ? days + 'd' : `${-days}d 超`) : '暂无'}</div>
        </div>
        <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
          <IjLabel>人工标准完成情况</IjLabel>
          <div style={{ fontSize: 26, fontWeight: 700, fontFamily: 'var(--font-mono)', color: criteria.length && metCount === criteria.length ? 'var(--pos)' : 'var(--text)' }}>{criteria.length ? `${metCount}/${criteria.length}` : '暂无'}</div>
        </div>
      </div>

      {/* assessments */}
      <div className="injury-assessment-table" style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px', overflowX: 'auto' }}>
        <IjLabel style={{ marginBottom: 8 }}>Assessments 评估测试（患侧 / 健侧 → LSI；可对比基线 & 健侧）</IjLabel>
        <div style={{ display: 'grid', gridTemplateColumns: '110px 1fr 90px 90px 70px 28px', gap: 7, alignItems: 'center', fontSize: 9.5, color: 'var(--muted)', marginBottom: 4 }}>
          <span>Date</span><span>Test</span><span style={{ textAlign: 'center' }}>Injured</span><span style={{ textAlign: 'center' }}>Healthy</span><span style={{ textAlign: 'center' }}>LSI</span><span/>
        </div>
        {sorted.length === 0 && assessments.length === 0 && <div style={{ fontSize: 11.5, color: 'var(--muted)', padding: '6px 0' }}>暂无评估记录。</div>}
        {assessments.map((a, i) => (
          <div key={a.id} style={{ display: 'grid', gridTemplateColumns: '110px 1fr 90px 90px 70px 28px', gap: 7, alignItems: 'center', marginBottom: 4, fontSize: 12 }}>
            <span style={{ color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>{a.date}</span>
            <span style={{ fontWeight: 500 }}>{a.name}</span>
            <span style={{ textAlign: 'center', fontFamily: 'var(--font-mono)' }}>{a.injured}</span>
            <span style={{ textAlign: 'center', fontFamily: 'var(--font-mono)' }}>{a.healthy}</span>
            <span style={{ textAlign: 'center', fontFamily: 'var(--font-mono)', fontWeight: 700, color: a.lsi == null ? 'var(--muted)' : a.lsi >= 90 ? 'var(--pos)' : a.lsi >= 75 ? 'var(--warn)' : 'var(--neg)' }}>{a.lsi != null ? a.lsi + '%' : '暂无'}</span>
            <button onClick={() => removeAssessment(i)} style={{ background: 'none', border: 0, color: 'var(--muted-2)', cursor: 'pointer', fontSize: 13 }}>×</button>
          </div>
        ))}
        {/* add row */}
        <div style={{ display: 'grid', gridTemplateColumns: '110px 1fr 90px 90px 70px 28px', gap: 7, alignItems: 'center', marginTop: 8, paddingTop: 8, borderTop: '1px solid var(--border)' }}>
          <input type="date" value={draft.date} onChange={e => setDraft(d => ({ ...d, date: e.target.value }))} style={{ ...ijInput, padding: '5px 6px', fontSize: 11 }}/>
          <input value={draft.name} onChange={e => setDraft(d => ({ ...d, name: e.target.value }))} placeholder="e.g. Single-leg hop (cm)" style={{ ...ijInput, padding: '5px 8px' }}/>
          <input value={draft.injured} onChange={e => setDraft(d => ({ ...d, injured: e.target.value }))} placeholder="患" style={{ ...ijInput, padding: '5px 6px', textAlign: 'center' }}/>
          <input value={draft.healthy} onChange={e => setDraft(d => ({ ...d, healthy: e.target.value }))} placeholder="健" style={{ ...ijInput, padding: '5px 6px', textAlign: 'center' }}/>
          <button className="btn primary" style={{ fontSize: 11, padding: '5px 0', gridColumn: 'span 2' }} onClick={addAssessment}>+ Add</button>
        </div>
        {draftError && <div role="alert" className="injury-inline-error">{draftError}</div>}
      </div>

      {/* RTP criteria (optional, user-defined) */}
      <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px' }}>
        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
          <IjLabel>Return-to-play criteria 重返标准（可选 · 自定义）</IjLabel>
          <button className="btn" style={{ fontSize: 11, marginLeft: 'auto', padding: '3px 8px' }} onClick={addCriterion}>+ Criterion</button>
        </div>
        {criteria.length === 0 && <div style={{ fontSize: 11.5, color: 'var(--muted)' }}>未设标准。可按需添加（如 LSI≥90%、无痛全负荷跑、完成专项动作）。</div>}
        {criteria.map((c, i) => (
          <div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
            <button onClick={() => updCriterion(i, { met: !c.met })} style={{ width: 24, height: 24, borderRadius: 6, cursor: 'pointer', flexShrink: 0, background: c.met ? 'var(--pos)' : 'var(--panel-hi)', color: c.met ? '#fff' : 'var(--muted)', border: `1px solid ${c.met ? 'var(--pos)' : 'var(--border)'}` }}>{c.met ? '✓' : '○'}</button>
            <input value={c.label} onChange={e => updCriterion(i, { label: e.target.value })} placeholder="criterion…" style={{ ...ijInput, flex: 1 }}/>
            <button onClick={() => removeCriterion(i)} style={{ background: 'none', border: 0, color: 'var(--muted-2)', cursor: 'pointer', fontSize: 14 }}>×</button>
          </div>
        ))}
      </div>
    </div>
  );
}

Object.assign(window, { InjuryView });
