/* =====================================================================
   経営サポート（経営相談室 / アクションプラン / レポート）
   - 要件定義書 v1.1 に基づく移設実装
   - 既存アプリの認証・サイドバー・トースト・店舗マスター（STORES）を流用
   - 保存先: localStorage 'ipb_mgmt_v1'（既存キーには一切触れない）
   - 相談は「定型ガイド」。AI自由回答／ニュース自動取得／実配信は含まない
   - CSS は .ms- プレフィックスのみ使用
   ===================================================================== */
(function () {
  const { useState, useEffect, useMemo, useRef, useCallback } = React;

  /* ---------- 保存基盤 ---------- */
  const MS_KEY = 'ipb_mgmt_v1';
  const GUIDE_VERSION = '1.1.0';
  const emptyStore = () => ({ version: 1, plans: [], actions: [], reports: [], bookmarks: [], settings: { previewChannel: 'mail', previewTo: '' } });
  function readStore() {
    try { const o = JSON.parse(localStorage.getItem(MS_KEY) || 'null'); if (o && typeof o === 'object') return { ...emptyStore(), ...o, settings: { ...emptyStore().settings, ...(o.settings || {}) } }; } catch {}
    return emptyStore();
  }
  function writeStore(s) { try { localStorage.setItem(MS_KEY, JSON.stringify(s)); } catch {} }
  const uid = () => `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
  const nowISO = () => new Date().toISOString();
  const fmtDate = (iso) => { if (!iso) return '—'; const d = new Date(iso); return isNaN(d) ? String(iso) : `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`; };
  const todayStr = () => new Date().toISOString().slice(0, 10);
  const monthStr = (iso) => String(iso || '').slice(0, 7);

  function useMgmtStore() {
    const [store, setStore] = useState(readStore);
    useEffect(() => { writeStore(store); }, [store]);
    const update = useCallback((fn) => setStore(prev => { const next = fn(structuredClone(prev)); return next; }), []);
    return [store, update];
  }

  /* ---------- 定型ガイド（5テーマ） ----------
     構造: theme → interests(複数選択) → readiness → guide
     出力は「選択内容の整理 / 仮説・着眼点 / 判断前に確認したい情報 / 対応の選択肢(メリット・注意点・確認条件) / 次のアクション案」
     ※ 仮説は仮説として表示し、断定しない。
  */
  const THEMES = [
    {
      id: 'economy', label: '日本経済と経営判断', icon: 'fa-chart-line',
      desc: '金利・物価・賃金・為替などのマクロ環境を、5店舗の経営判断にどう結びつけるか',
      interests: [
        { id: 'rates', label: '金利上昇・借入コスト' }, { id: 'prices', label: '物価上昇・仕入コスト' },
        { id: 'wages', label: '賃金動向・人件費' }, { id: 'region', label: '北陸・石川の地域経済' },
        { id: 'policy', label: '税制・社会保障の改定' },
      ],
      hypotheses: {
        rates:  '金利上昇が続く場合、借入の金利負担と新規投資の採算が変わる可能性がある（仮説）。',
        prices: '物価上昇は薬剤以外の消耗品・光熱費・配送費から効いてくる可能性がある（仮説）。薬剤は薬価で売価が固定されるため、価格転嫁ができない点が他業種と異なる。',
        wages:  '地域の賃金水準が上がると、薬剤師・事務の採用競争と既存社員の処遇見直し圧力が同時に来る可能性がある（仮説）。',
        region: '北陸の景況感と人口動態（能登の転出超過など）は、店舗ごとの患者数に異なる影響を与える可能性がある（仮説）。',
        policy: '税制・社会保障の改定は、患者の受診行動（窓口負担）と自社の資金繰り（納税・社会保険料）の両方に効く可能性がある（仮説）。',
      },
      checks: [
        '自社の借入残高・金利タイプ（固定/変動）・返済スケジュール',
        '直近12ヶ月の費目別コスト推移（薬剤費以外）',
        '店舗別の処方箋枚数トレンドと処方元の構成（処方元TOP3ページ）',
        '石川県の最低賃金・同業の求人賃金の水準',
        '関連する制度改定のスケジュール（施行日・経過措置）',
      ],
      options: [
        { id: 'A', title: '様子見・情報収集を優先', merit: '判断を急がず、誤った固定費増を避けられる', caution: '機会損失（採用・投資の遅れ）が生じる可能性', condition: '次回の幹部会までに確認項目が揃うこと' },
        { id: 'B', title: '変動要因ごとに感度分析を行う', merit: '金利+1%、人件費+3% などの影響額を事前に把握できる', caution: '前提の置き方で結果が大きく変わる', condition: 'IPBの月次データ（売上・技術料・枚数）が最新であること' },
        { id: 'C', title: '優先度の高い1テーマだけ即対応', merit: '小さく早く動ける', caution: '他のテーマが後回しになる', condition: '対応テーマの判断材料が揃っていること' },
      ],
      actions: [
        { text: '借入一覧（残高・金利・返済期日）を整理する', owner: '経理担当' },
        { text: '費目別コスト推移（直近12ヶ月）を作成する', owner: '経理担当' },
        { text: '幹部会で「経済環境の論点」を1枚にまとめて共有する', owner: '経営者' },
      ],
    },
    {
      id: 'wages', label: '賃上げと収益の両立', icon: 'fa-coins',
      desc: '処遇改善と収益のバランス。調剤ベースアップ評価料などの制度活用も含めて整理',
      interests: [
        { id: 'base', label: 'ベースアップの水準' }, { id: 'eval', label: '調剤ベースアップ評価料の活用' },
        { id: 'retain', label: '離職防止・定着' }, { id: 'prod', label: '生産性（枚数/人・技術料/枚）' },
        { id: 'parttime', label: 'パート・事務の処遇' },
      ],
      hypotheses: {
        base:    '賃上げ率は「地域水準」「評価料でまかなえる範囲」「技術料の伸び」の3つで上限が決まる可能性がある（仮説）。',
        eval:    '調剤ベースアップ評価料は要件（賃金改善計画・実績報告）を満たせば原資になり得るが、全額をカバーできるとは限らない（仮説）。',
        retain:  '離職は賃金だけでなく、シフト・業務負荷・キャリアの見通しが要因になっている可能性がある（仮説）。',
        prod:    '技術料/枚と枚数/人の改善で賃上げ原資を作れる店舗と、構造上難しい店舗がある可能性がある（仮説）。',
        parttime:'パート・事務は最低賃金改定の影響を直接受けるため、正社員より先に見直しが必要になる可能性がある（仮説）。',
      },
      checks: [
        '店舗別の人件費率（人件費 ÷ 技術料合計）',
        '職種別の現在の賃金水準と地域相場の差',
        '評価料の算定要件と、自社の届出・計画の状況',
        '直近1年の離職者数と退職理由（分かる範囲で）',
        '店舗別の枚数/人・技術料/枚（IPBダッシュボード）',
      ],
      options: [
        { id: 'A', title: '一律ベースアップ', merit: '公平感があり説明しやすい', caution: '収益の弱い店舗の負担が重くなる', condition: '全社の人件費率が許容範囲に収まること' },
        { id: 'B', title: '評価料の範囲内＋評価連動の上乗せ', merit: '原資と連動し、頑張りが報われる設計にできる', caution: '評価基準の納得感が必要。制度要件の確認が必須', condition: '評価料の要件を満たし、評価基準を文書化できること' },
        { id: 'C', title: '非金銭的処遇（シフト・休暇・教育）を先行', merit: '固定費を増やさず定着に効く可能性', caution: '賃金への不満が強い場合は効果が限定的', condition: '離職理由の把握ができていること' },
      ],
      actions: [
        { text: '店舗別の人件費率と技術料/枚を一覧化する', owner: '経理担当' },
        { text: '評価料の算定要件と自社の届出状況を確認する', owner: '管理薬剤師' },
        { text: '職種別の地域賃金相場を調べる', owner: '経営者' },
      ],
    },
    {
      id: 'invest', label: '設備投資・資金繰り', icon: 'fa-building',
      desc: '調剤機器・DX投資・改装などの投資判断と、資金繰りの見通し',
      interests: [
        { id: 'machine', label: '調剤機器・自動化' }, { id: 'dx', label: '電子処方箋・DX対応' },
        { id: 'renov', label: '改装・移転・新規出店' }, { id: 'cash', label: '資金繰り・借入' },
        { id: 'subsidy', label: '補助金・助成金' },
      ],
      hypotheses: {
        machine: '自動化投資は「人件費削減」より「人手不足下でも回る体制」として評価する方が実態に合う可能性がある（仮説）。',
        dx:      'DX対応は制度上の必須部分と任意部分を分けて考えると、投資の優先順位が明確になる可能性がある（仮説）。',
        renov:   '改装・出店は処方元の動向（病院の診療体制変更など）に依存するため、処方元TOP3の集中度を先に確認する必要がある（仮説）。',
        cash:    '金利上昇局面では、変動金利借入の見直しと手元資金の厚みが先に効く可能性がある（仮説）。',
        subsidy: '補助金は申請・実績報告の事務負担があり、担当者の時間を含めて採算を見る必要がある（仮説）。',
      },
      checks: [
        '投資額・耐用年数・年間の削減効果（人時 or 金額）の見積り',
        '月次の資金繰り表（3〜6ヶ月先まで）',
        '対象店舗の処方箋枚数トレンドと処方元の集中度',
        '利用可能な補助金の公募時期・要件',
        '借入の金利タイプと残高',
      ],
      options: [
        { id: 'A', title: '投資を見送り、現状維持', merit: 'キャッシュを温存できる', caution: '人手不足・制度対応で後手に回る可能性', condition: '現状の体制で制度要件を満たせること' },
        { id: 'B', title: '1店舗で試験導入し、効果を測ってから展開', merit: 'リスクを限定し、実データで判断できる', caution: '展開まで時間がかかる', condition: '試験店舗の効果測定指標を事前に決められること' },
        { id: 'C', title: '補助金の採択を条件に全店導入', merit: '自己負担を抑えて一気に体制を整えられる', caution: '不採択時の代替案が必要。事務負担が大きい', condition: '公募要件を満たし、申請担当を確保できること' },
      ],
      actions: [
        { text: '投資案件ごとに「投資額・効果・回収期間」の1枚を作る', owner: '経営者' },
        { text: '3〜6ヶ月の資金繰り表を更新する', owner: '経理担当' },
        { text: '対象店舗の処方元集中度を確認する（処方元TOP3）', owner: '店舗責任者' },
      ],
    },
    {
      id: 'people', label: '人材定着・店舗運営', icon: 'fa-users',
      desc: '薬剤師・事務の定着、シフト、業務負荷、店舗間の応援体制',
      interests: [
        { id: 'turnover', label: '離職・採用' }, { id: 'shift', label: 'シフト・残業' },
        { id: 'load', label: '業務負荷・ヒヤリハット' }, { id: 'support', label: '店舗間の応援・多店舗運営' },
        { id: 'career', label: '教育・キャリア' },
      ],
      hypotheses: {
        turnover: '離職は「辞める直前」ではなく数ヶ月前の兆候（残業増・有休未消化）に表れている可能性がある（仮説）。',
        shift:    '門前店（南ヶ丘病院店）と面対応店では繁忙の山が異なるため、店舗横断のシフト設計で負荷を平準化できる可能性がある（仮説）。',
        load:     'ヒヤリハットの増加は人手不足や業務集中のサインである可能性がある（仮説）。件数そのものより傾向を見る。',
        support:  '応援体制は「誰がどこへ行けるか」を事前に決めておくと、急な欠員でも店舗を止めずに済む可能性がある（仮説）。',
        career:   '教育・キャリアの見通しがあると、賃金以外の定着要因になる可能性がある（仮説）。',
      },
      checks: [
        '店舗別の在籍人数・残業時間・有休消化率',
        '直近の離職者数と退職理由',
        '店舗別の枚数/人（IPBダッシュボード）と繁忙時間帯',
        'ヒヤリハット件数の推移と内容',
        '店舗間で応援可能な人員のリスト',
      ],
      options: [
        { id: 'A', title: '店舗ごとに個別対応', merit: '現場の実情に合わせやすい', caution: '店舗間で処遇差が生じ不満の原因になる可能性', condition: '店舗責任者が判断材料を持っていること' },
        { id: 'B', title: '全社ルール（シフト・応援・教育）を整備', merit: '公平で説明しやすく、多店舗運営に強い', caution: '整備に時間がかかり、現場の裁量が減る', condition: '幹部会で合意形成できること' },
        { id: 'C', title: 'まず1店舗で負荷軽減策を試す', merit: '小さく始めて効果を確認できる', caution: '他店舗への展開が遅れる', condition: '試す店舗と測る指標（残業・ヒヤリハット）を決められること' },
      ],
      actions: [
        { text: '店舗別の残業・有休消化・在籍人数を一覧化する', owner: '経理担当' },
        { text: '応援可能人員と移動可能日をリスト化する', owner: '管理薬剤師' },
        { text: '幹部会で店舗運営の論点を共有する', owner: '経営者' },
      ],
    },
    {
      id: 'plan', label: '事業計画・幹部会', icon: 'fa-people-group',
      desc: '年度計画・目標設定・幹部会での意思決定の進め方',
      interests: [
        { id: 'target', label: '売上・技術料の目標設定' }, { id: 'kpi', label: 'KPIの選び方' },
        { id: 'meeting', label: '幹部会の進め方' }, { id: 'scenario', label: 'シナリオ・リスク' },
        { id: 'succession', label: '事業承継・組織' },
      ],
      hypotheses: {
        target:    '目標は「前年比」だけでなく、処方元の動向と制度改定の影響を織り込んだ方が現実的になる可能性がある（仮説）。',
        kpi:       'KPIは数を絞り、技術料合計・技術料/枚・後発品率・枚数/人 など自社で動かせる指標を中心にする方が運用しやすい可能性がある（仮説）。',
        meeting:   '幹部会は「報告」より「判断が必要な論点」を先に置くと、決定事項が増える可能性がある（仮説）。',
        scenario:  '基本・上振れ・下振れの3シナリオを置くと、下振れ時の対応を事前に決めておける可能性がある（仮説）。',
        succession:'承継期は「数字」と「判断の理由」をセットで記録しておくと、後からの説明と引き継ぎがしやすくなる可能性がある（仮説）。',
      },
      checks: [
        '直近年度の実績（IPB 前年同月比・年度累計）',
        '制度改定スケジュールと影響が見込まれる項目',
        '処方元TOP3の集中度と変化',
        '前回の幹部会の決定事項と進捗',
        '来期の人員計画・投資計画の有無',
      ],
      options: [
        { id: 'A', title: '前年実績ベースの目標設定', merit: '作りやすく、現場に説明しやすい', caution: '環境変化（制度・処方元）を反映しにくい', condition: '大きな環境変化が見込まれないこと' },
        { id: 'B', title: '3シナリオで目標と対応策をセットで決める', merit: '下振れ時の初動が早くなる', caution: '作成の手間が増える', condition: '主要な変動要因を特定できていること' },
        { id: 'C', title: 'KPIを3〜5個に絞って月次で追う', merit: '幹部会の論点が明確になる', caution: 'KPIに表れない問題を見落とす可能性', condition: 'KPIの定義と算出方法を共有できること' },
      ],
      actions: [
        { text: '来期のKPI候補を3〜5個に絞る', owner: '経営者' },
        { text: '制度改定スケジュールと影響項目を一覧化する', owner: '管理薬剤師' },
        { text: '幹部会のアジェンダを「判断事項→報告」の順に組み替える', owner: '経営者' },
      ],
    },
  ];
  const READINESS = [
    { id: 'none',   label: 'まだ分からない', note: '判断材料が揃っていない前提でガイドを表示します。断定的な結論は出しません。' },
    { id: 'gather', label: 'これから情報を集める', note: '「判断前に確認したい情報」を優先して表示します。' },
    { id: 'partial',label: '一部は手元にある', note: '手元の情報で検討できる選択肢と、追加で必要な情報を分けて表示します。' },
    { id: 'ready',  label: '判断材料は揃っている', note: '選択肢の比較と次のアクションを中心に表示します。' },
  ];
  const OWNERS = ['経営者', '経理担当', '管理薬剤師', '店舗責任者', '事務リーダー', '外部専門家（税理士・社労士）'];

  /* ガイド生成：選択内容 → 表示用オブジェクト
     learnings: 学習ノート（セミナーの学び）から関連度順に選んだ項目。仮説・確認事項・アクション案に「学び」として合流させる（事実ではなく学びとして明示） */
  function buildGuide(theme, answers, learnings) {
    const interests = (answers.interests || []).map(id => theme.interests.find(i => i.id === id)).filter(Boolean);
    const readiness = READINESS.find(r => r.id === answers.readiness) || READINESS[0];
    const scopeLabel = answers.scope === 'all' ? '5店舗全体' : (answers.storeNames || []).join('・') || '（店舗未選択）';
    const hyps = interests.length ? interests.map(i => ({ interest: i.label, text: theme.hypotheses[i.id] })) : [{ interest: '全般', text: 'テーマに関する関心事を選ぶと、該当する仮説・着眼点を表示します。' }];
    const limited = readiness.id === 'none' || readiness.id === 'gather';
    const L = Array.isArray(learnings) ? learnings : [];
    // 学びからの合流（各学びから少数ずつ・重複は除く）
    const learnHyps = []; const learnChecks = []; const learnActs = [];
    L.forEach((k, i) => {
      const ref = `[L${i + 1}] ${k.title}`;
      (k.applications || []).slice(0, 2).forEach(t => learnHyps.push({ interest: '学び', text: t, ref, learnId: k.id }));
      (k.checklist || []).slice(0, 2).forEach(t => learnChecks.push({ text: t, ref, learnId: k.id }));
      (k.principles || []).slice(0, 1).forEach(t => learnActs.push({ text: `「${t}」を自社の数字で検証する（${k.title}より）`, owner: '経営者', ref, learnId: k.id }));
    });
    const seen = new Set(theme.checks);
    const checksAll = [...theme.checks, ...learnChecks.filter(c => !seen.has(c.text) && (seen.add(c.text), true)).map(c => `${c.text}（学び：${c.ref}）`)];
    return {
      summary: [
        `相談テーマ：${theme.label}`,
        `関心事：${interests.length ? interests.map(i => i.label).join('、') : '（未選択）'}`,
        `対象範囲：${scopeLabel}`,
        `判断材料：${readiness.label}`,
        answers.memo ? `メモ：${answers.memo}` : null,
        L.length ? `参照した学び：${L.length}件（${L.map((k, i) => `[L${i + 1}] ${k.title}`).join('、')}）` : null,
      ].filter(Boolean),
      hypotheses: [...hyps, ...learnHyps.slice(0, 4)],
      checks: checksAll,
      options: theme.options,
      actions: [...theme.actions, ...learnActs.slice(0, 2)],
      learnings: L,
      readinessNote: readiness.note,
      limited,
      caveat: limited
        ? '判断材料が揃っていないため、投資の是非・賃上げ率・人員配置などの結論は示しません。まず「判断前に確認したい情報」を揃えることを推奨します。'
        : '選択肢は比較のための整理であり、「最適」「採用済み」を自動判定するものではありません。最終判断は確認条件を満たしたうえで行ってください。',
    };
  }

  /* ---------- 共通小部品 ---------- */
  const Tag = ({ kind, children }) => <span className={`ms-tag ms-tag-${kind}`}>{children}</span>;
  const Empty = ({ icon = 'fa-inbox', title, sub }) => (
    <div className="ms-empty"><i className={`fas ${icon}`}></i><div className="ms-empty-title">{title}</div>{sub && <div className="ms-empty-sub">{sub}</div>}</div>
  );
  function ConfirmDialog({ title, body, onOk, onCancel, okLabel = '削除する' }) {
    return (
      <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onCancel(); }}>
        <div className="modal-card ms-confirm">
          <div className="modal-head"><div className="modal-title"><i className="fas fa-triangle-exclamation mr-2"></i>{title}</div><button className="modal-close" onClick={onCancel}><i className="fas fa-xmark"></i></button></div>
          <div className="modal-body"><div className="text-[13px] mb-4">{body}</div>
            <div className="flex gap-2 justify-end"><button className="btn btn-secondary" onClick={onCancel}>キャンセル</button><button className="btn btn-danger" id="ms-confirm-ok" onClick={onOk}>{okLabel}</button></div>
          </div>
        </div>
      </div>
    );
  }
  const copyText = async (txt, toast, label = 'コピーしました') => { try { await navigator.clipboard.writeText(txt); toast?.(label); } catch { alert('コピーに失敗しました。手動で選択してコピーしてください。'); } };
  const downloadText = (name, txt) => { const blob = new Blob([txt], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1500); };

  /* ---------- ツリー表示 ---------- */
  function TreeNode({ label, kind, children, defaultOpen = true, meta }) {
    const [open, setOpen] = useState(defaultOpen);
    const has = React.Children.count(children) > 0;
    return (
      <li className={`ms-tree-node ${open ? 'is-open' : ''}`}>
        <div className="ms-tree-row">
          {has ? <button type="button" className="ms-tree-toggle" onClick={() => setOpen(o => !o)} aria-label={open ? '閉じる' : '開く'}><i className={`fas ${open ? 'fa-minus' : 'fa-plus'}`}></i></button> : <span className="ms-tree-dot"></span>}
          {kind && <Tag kind={kind}>{{ hyp: '仮説', fact: '判断材料', opt: '選択肢', act: 'アクション', info: '整理', learn: '学び' }[kind] || kind}</Tag>}
          <span className="ms-tree-label">{label}</span>
          {meta && <span className="ms-tree-meta">{meta}</span>}
        </div>
        {has && open && <ul className="ms-tree-children">{children}</ul>}
      </li>
    );
  }
  function TreeView({ theme, guide, plan, actions }) {
    const savedActs = actions || [];
    return (
      <ul className="ms-tree" id="ms-tree">
        <TreeNode label={theme.label} meta={plan?.updatedAt ? `更新 ${fmtDate(plan.updatedAt)}` : '未保存'}>
          <TreeNode label="選択内容の整理" kind="info" defaultOpen={false}>
            {guide.summary.map((s, i) => <TreeNode key={i} label={s} />)}
          </TreeNode>
          <TreeNode label="仮説・着眼点（事実ではありません）" kind="hyp">
            {guide.hypotheses.map((h, i) => <TreeNode key={i} label={h.text} meta={h.ref ? `${h.interest}：${h.ref}` : h.interest} kind={h.learnId ? 'learn' : undefined} />)}
          </TreeNode>
          {guide.learnings?.length > 0 && (
            <TreeNode label="関連する学び（セミナー等の学習ノート・事実ではなく学びとして参照）" kind="learn">
              {guide.learnings.map((k, i) => (
                <TreeNode key={k.id} label={`[L${i + 1}] ${k.title}`} meta={k.source || k.date || ''}>
                  {(k.principles || []).slice(0, 3).map((p, j) => <TreeNode key={`p${j}`} label={`原則：${p}`} />)}
                  {(k.numbers || []).slice(0, 2).map((p, j) => <TreeNode key={`n${j}`} label={`数字：${p}`} />)}
                </TreeNode>
              ))}
            </TreeNode>
          )}
          <TreeNode label="判断前に確認したい情報" kind="fact">
            {guide.checks.map((c, i) => <TreeNode key={i} label={c} />)}
          </TreeNode>
          <TreeNode label="対応の選択肢（比較用・優劣は判定しません）" kind="opt">
            {guide.options.map(o => (
              <TreeNode key={o.id} label={`選択肢${o.id}：${o.title}`}>
                <TreeNode label={`メリット：${o.merit}`} />
                <TreeNode label={`注意点：${o.caution}`} />
                <TreeNode label={`確認条件：${o.condition}`} />
              </TreeNode>
            ))}
          </TreeNode>
          <TreeNode label="次のアクション" kind="act">
            {savedActs.length
              ? savedActs.map(a => <TreeNode key={a.id} label={a.text} meta={`${a.owner || '担当未定'}／期限 ${a.due ? fmtDate(a.due) : '未定'}／${a.done ? '作業完了（効果は未検証）' : '未完了'}`} />)
              : guide.actions.map((a, i) => <TreeNode key={i} label={a.text} meta={`案：${a.owner}（未保存）`} />)}
          </TreeNode>
        </TreeNode>
      </ul>
    );
  }

  /* ---------- PDCA表示 ---------- */
  function PdcaView({ theme, guide, actions }) {
    const acts = actions || [];
    const doneN = acts.filter(a => a.done).length;
    const stages = [
      { id: 'P', label: 'Plan：計画', icon: 'fa-lightbulb', body: (
        <>
          <div className="ms-pdca-sub">論点</div><ul>{guide.summary.map((s, i) => <li key={i}>{s}</li>)}</ul>
          <div className="ms-pdca-sub">仮説 <Tag kind="hyp">仮説</Tag></div><ul>{guide.hypotheses.map((h, i) => <li key={i}>{h.text}</li>)}</ul>
          <div className="ms-pdca-sub">判断材料</div><ul>{guide.checks.map((c, i) => <li key={i}>{c}</li>)}</ul>
          <div className="ms-pdca-sub">比較する選択肢</div><ul>{guide.options.map(o => <li key={o.id}><b>選択肢{o.id}</b> {o.title} — 確認条件：{o.condition}</li>)}</ul>
        </>
      ) },
      { id: 'D', label: 'Do：実行', icon: 'fa-person-running', body: (
        acts.length
          ? <ul className="ms-pdca-acts">{acts.map(a => <li key={a.id} className={a.done ? 'is-done' : ''}><i className={`fas ${a.done ? 'fa-square-check' : 'fa-square'}`}></i><span>{a.text}</span><span className="ms-pdca-act-meta">{a.owner || '担当未定'}／{a.due ? fmtDate(a.due) : '期限未定'}</span></li>)}</ul>
          : <div className="ms-note">アクションは未保存です。「詳細」タブのアクション案から保存すると、ここに担当・期限・完了状況が表示されます。</div>
      ), foot: acts.length ? `作業完了 ${doneN}/${acts.length} 件（作業の完了であり、効果の確認ではありません）` : null },
      { id: 'C', label: 'Check：検証', icon: 'fa-magnifying-glass-chart', body: (
        <>
          <div className="ms-status ms-status-unverified"><i className="fas fa-circle-question"></i>効果は未検証（実績の記録機能は将来拡張）</div>
          <div className="ms-pdca-sub">確認指標の例</div>
          <ul>{checkMetricsFor(theme.id).map((m, i) => <li key={i}>{m}</li>)}</ul>
          <div className="ms-pdca-sub">比較方法の例</div><ul><li>施策前3ヶ月 vs 施策後3ヶ月（IPB 前年同月比ページで季節性も確認）</li><li>対象店舗 vs 非対象店舗</li></ul>
          <div className="ms-pdca-sub">振り返り時期の例</div><ul><li>実行1ヶ月後：作業完了の確認</li><li>3ヶ月後：指標の初期変化</li><li>6ヶ月後：効果判定と継続可否</li></ul>
        </>
      ) },
      { id: 'A', label: 'Act：改善', icon: 'fa-rotate', body: (
        <>
          <div className="ms-pdca-sub">効果が出た場合の例</div><ul><li>他店舗へ展開する（店舗の条件差を確認してから）</li><li>基準・ルールとして文書化する</li></ul>
          <div className="ms-pdca-sub">効果が薄い／負担が大きい場合の例</div><ul><li>対象や規模を縮小して継続</li><li>別の選択肢（{guide.options.map(o => o.id).join('/')}）に切り替える</li><li>判断材料を追加で集め直す</li></ul>
          <div className="ms-pdca-sub">次のPlanへ</div><ul><li>検証結果を踏まえて論点・仮説を更新し、新しい相談プランとして保存する</li></ul>
        </>
      ) },
    ];
    return (
      <div className="ms-pdca" id="ms-pdca">
        <div className="ms-pdca-cycle">
          {stages.map((s, i) => <React.Fragment key={s.id}><div className={`ms-pdca-pill ms-pdca-${s.id}`}><b>{s.id}</b>{s.label.split('：')[1]}</div>{i < 3 && <i className="fas fa-arrow-right"></i>}</React.Fragment>)}
          <i className="fas fa-arrow-right"></i><div className="ms-pdca-pill ms-pdca-next"><b>P</b>次の計画</div>
        </div>
        <div className="ms-pdca-grid">
          {stages.map(s => (
            <section key={s.id} className={`ms-pdca-card ms-pdca-${s.id}`}>
              <h4><i className={`fas ${s.icon}`}></i>{s.label}</h4>
              <div className="ms-pdca-body">{s.body}</div>
              {s.foot && <div className="ms-pdca-foot">{s.foot}</div>}
            </section>
          ))}
        </div>
      </div>
    );
  }
  function checkMetricsFor(themeId) {
    return {
      economy: ['借入金利負担（月額）', '費目別コストの対前年比', '店舗別 処方箋枚数の対前年比'],
      wages:   ['人件費率（人件費÷技術料合計）', '離職者数・有休消化率', '技術料/枚・枚数/人'],
      invest:  ['投資回収の進捗（削減人時・金額）', '月次資金繰りの余裕額', '対象店舗の枚数・待ち時間'],
      people:  ['残業時間・有休消化率', 'ヒヤリハット件数の傾向', '離職者数・採用充足率'],
      plan:    ['KPIの達成率（技術料合計・後発品率など）', '幹部会の決定事項の実行率', 'シナリオ前提と実績の乖離'],
    }[themeId] || [];
  }

  /* ---------- 詳細表示 ---------- */
  function DetailView({ theme, guide, plan, actions, onSaveActions, onToggleDone, toast }) {
    const [picked, setPicked] = useState(() => new Set(guide.actions.map((_, i) => i)));
    const [drafts, setDrafts] = useState(() => guide.actions.map(a => ({ text: a.text, owner: a.owner, due: '' })));
    useEffect(() => { setDrafts(guide.actions.map(a => ({ text: a.text, owner: a.owner, due: '' }))); setPicked(new Set(guide.actions.map((_, i) => i))); }, [theme.id]);
    const saved = actions || [];
    const savedTexts = new Set(saved.map(a => a.text.trim()));
    return (
      <div className="ms-detail" id="ms-detail">
        <section className="ms-sec"><h4><Tag kind="info">整理</Tag>選択内容の整理</h4><ul>{guide.summary.map((s, i) => <li key={i}>{s}</li>)}</ul><div className="ms-note">{guide.readinessNote}</div></section>
        <section className="ms-sec"><h4><Tag kind="hyp">仮説</Tag>仮説・着眼点</h4><ul>{guide.hypotheses.map((h, i) => <li key={i}><b>{h.interest}</b>：{h.text}{h.ref && <span className="ms-learn-ref"> — {h.ref}</span>}</li>)}</ul></section>
        {guide.learnings?.length > 0 && (
          <section className="ms-sec ms-sec-learn"><h4><Tag kind="learn">学び</Tag>関連する学び（学習ノートから自動参照）</h4>
            <ul>{guide.learnings.map((k, i) => <li key={k.id}><b>[L{i + 1}] {k.title}</b>{k.source ? `（${k.source}）` : ''}{(k.principles || []).length > 0 && <div className="ms-learn-mini">{k.principles.slice(0, 3).join('／')}</div>}</li>)}</ul>
            <div className="ms-note">セミナー等で経営者が学んだ内容です。事実やデータの裏付けとは別物として扱い、自社の数字で検証してください。</div>
          </section>
        )}
        <section className="ms-sec"><h4><Tag kind="fact">判断材料</Tag>判断前に確認したい情報</h4><ul>{guide.checks.map((c, i) => <li key={i}>{c}</li>)}</ul></section>
        <section className="ms-sec"><h4><Tag kind="opt">選択肢</Tag>対応の選択肢</h4>
          <div className="ms-opts">{guide.options.map(o => (
            <div key={o.id} className="ms-opt"><div className="ms-opt-title">選択肢{o.id}：{o.title}</div>
              <div><b>メリット</b> {o.merit}</div><div><b>注意点</b> {o.caution}</div><div><b>確認条件</b> {o.condition}</div></div>
          ))}</div>
          <div className="ms-caveat"><i className="fas fa-circle-info"></i>{guide.caveat}</div>
        </section>
        <section className="ms-sec"><h4><Tag kind="act">アクション</Tag>次のアクション案</h4>
          <div className="ms-note">保存したいものにチェックし、担当・期限を調整して「アクションを保存」。同じ内容は重複登録されません。</div>
          <div className="ms-act-drafts">{drafts.map((d, i) => { const dup = savedTexts.has(d.text.trim()); return (
            <div key={i} className={`ms-act-draft ${dup ? 'is-dup' : ''}`}>
              <input type="checkbox" checked={picked.has(i)} onChange={() => setPicked(p => { const n = new Set(p); n.has(i) ? n.delete(i) : n.add(i); return n; })} />
              <input className="ms-input flex-1" value={d.text} onChange={(e) => setDrafts(ds => ds.map((x, j) => j === i ? { ...x, text: e.target.value } : x))} />
              <select className="ms-input" value={d.owner} onChange={(e) => setDrafts(ds => ds.map((x, j) => j === i ? { ...x, owner: e.target.value } : x))}>{OWNERS.map(o => <option key={o}>{o}</option>)}</select>
              <input type="date" className="ms-input" value={d.due} onChange={(e) => setDrafts(ds => ds.map((x, j) => j === i ? { ...x, due: e.target.value } : x))} />
              {dup && <Tag kind="info">保存済み</Tag>}
            </div>
          ); })}</div>
          <div className="flex gap-2 flex-wrap mt-2">
            <button className="btn btn-secondary" onClick={() => setDrafts(ds => [...ds, { text: '', owner: OWNERS[0], due: '' }])}><i className="fas fa-plus"></i>行を追加</button>
            <button className="btn btn-primary" id="ms-save-actions" onClick={() => { const sel = drafts.filter((d, i) => picked.has(i) && d.text.trim()); if (!sel.length) { toast?.('保存するアクションを選んでください'); return; } onSaveActions(sel); }}><i className="fas fa-floppy-disk"></i>アクションを保存</button>
          </div>
          {saved.length > 0 && (
            <div className="mt-3"><div className="ms-pdca-sub">保存済みアクション（{saved.length}件）</div>
              <ul className="ms-pdca-acts">{saved.map(a => <li key={a.id} className={a.done ? 'is-done' : ''}><button type="button" className="ms-check" onClick={() => onToggleDone(a.id)}><i className={`fas ${a.done ? 'fa-square-check' : 'fa-square'}`}></i></button><span>{a.text}</span><span className="ms-pdca-act-meta">{a.owner}／{a.due ? fmtDate(a.due) : '期限未定'}{a.done ? '／作業完了（効果は未検証）' : ''}</span></li>)}</ul>
            </div>
          )}
        </section>
      </div>
    );
  }

  /* ---------- Markdown 出力 ---------- */
  function planToMarkdown(theme, guide, plan, actions) {
    const L = [];
    L.push(`# 経営相談：${theme.label}`, '', `- 作成: ${fmtDate(plan?.createdAt)} ／ 更新: ${fmtDate(plan?.updatedAt)} ／ ガイド v${plan?.guideVersion || GUIDE_VERSION}`, '');
    L.push('## 選択内容の整理'); guide.summary.forEach(s => L.push(`- ${s}`)); L.push('');
    L.push('## 仮説・着眼点（仮説であり事実ではありません）'); guide.hypotheses.forEach(h => L.push(`- **${h.interest}**：${h.text}${h.ref ? `（${h.ref}）` : ''}`)); L.push('');
    if (guide.learnings?.length) { L.push('## 関連する学び（学習ノート）'); guide.learnings.forEach((k, i) => L.push(`- [L${i + 1}] ${k.title}${k.source ? `（${k.source}）` : ''}：${(k.principles || []).slice(0, 3).join('／')}`)); L.push(''); }
    L.push('## 判断前に確認したい情報'); guide.checks.forEach(c => L.push(`- [ ] ${c}`)); L.push('');
    L.push('## 対応の選択肢（比較用）'); guide.options.forEach(o => { L.push(`### 選択肢${o.id}：${o.title}`, `- メリット：${o.merit}`, `- 注意点：${o.caution}`, `- 確認条件：${o.condition}`, ''); });
    L.push('## アクションと進捗');
    if (actions?.length) actions.forEach(a => L.push(`- [${a.done ? 'x' : ' '}] ${a.text}（担当：${a.owner || '未定'}／期限：${a.due ? fmtDate(a.due) : '未定'}）${a.done ? ' ※作業完了・効果は未検証' : ''}`));
    else L.push('- （未保存）');
    L.push('', '## 検証（Check）', '- 効果は未検証。確認指標・比較方法・振り返り時期はPDCA表示を参照。', '', `> ${guide.caveat}`);
    return L.join('\n');
  }

  /* ---------- 自由記入相談（AI）：SSE を読み、先頭行の分類JSON と本文を分離 ---------- */
  async function streamConsult({ question, kpi, stores, knowledge, history, interestsCatalog, signal, onMeta, onText }) {
    const res = await fetch('/api/consult', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ question, kpi, stores, knowledge: knowledge?.length ? knowledge : undefined, history, interestsCatalog }), signal });
    if (!res.ok || !res.body) { let d = ''; try { d = (await res.json()).error || ''; } catch {} throw new Error(d || `HTTP ${res.status}`); }
    const reader = res.body.getReader(); const dec = new TextDecoder('utf-8');
    let buf = '', acc = '', metaDone = false, meta = null;
    const emit = () => {
      if (!metaDone) {
        const nl = acc.indexOf('\n');
        const firstLine = nl >= 0 ? acc.slice(0, nl) : null;
        if (firstLine != null) {
          const m = firstLine.match(/\{[\s\S]*\}/);
          if (m) { try { meta = JSON.parse(m[0]); } catch { meta = null; } }
          metaDone = true; onMeta?.(meta);
          onText(acc.slice(nl + 1).replace(/^\s*```(?:markdown)?\s*/, ''));
        } else if (acc.length > 600 && !/^\s*\{/.test(acc)) { metaDone = true; onMeta?.(null); onText(acc); }
      } else onText(acc.indexOf('\n') >= 0 && /^\s*\{/.test(acc) ? acc.slice(acc.indexOf('\n') + 1) : acc);
    };
    while (true) {
      const { value, done } = await reader.read(); if (done) break;
      buf += dec.decode(value, { stream: true });
      let idx;
      while ((idx = buf.indexOf('\n\n')) >= 0) {
        const ev = buf.slice(0, idx); buf = buf.slice(idx + 2);
        for (const line of ev.split(/\r?\n/)) {
          const m = line.match(/^data:\s?(.*)$/); if (!m || m[1] === '[DONE]') continue;
          try { const j = JSON.parse(m[1]); if (j?.error) throw new Error(typeof j.error === 'string' ? j.error : JSON.stringify(j.error)); const d = j?.choices?.[0]?.delta?.content ?? ''; if (d) { acc += d; emit(); } } catch (e) { if (!(e instanceof SyntaxError)) throw e; }
        }
      }
    }
    if (!metaDone) { metaDone = true; const nl = acc.indexOf('\n'); const fl = nl >= 0 ? acc.slice(0, nl) : acc; const m = fl.match(/\{[\s\S]*\}/); if (m) { try { meta = JSON.parse(m[0]); } catch {} } onMeta?.(meta); onText(nl >= 0 ? acc.slice(nl + 1) : (meta ? '' : acc)); }
    return { meta, text: acc };
  }
  /* AI回答本文の [L1] をバッジ化（app.jsx の renderLearnCites があれば使う） */
  function renderConsultMd(md) {
    const nodes = renderMd(md);
    return nodes;
  }

  /* ---------- AI回答「次の一歩」→ アクション候補 ----------
     「## 次の一歩」セクションの箇条書きを解析。「（担当：X／期限：Y）」「担当: X」などから担当・期限の目安を推定 */
  const OWNER_HINTS = [
    [/経営者|社長|あなた|本部|オーナー|後継/, '経営者'], [/経理|財務|税理士|社労士|会計/, '経理担当'],
    [/管理薬剤師|薬局長|薬剤師リーダー/, '管理薬剤師'], [/店長|店舗責任者|店舗長|エリアマネ/, '店舗責任者'],
    [/事務|受付|医療事務/, '事務リーダー'], [/外部|コンサル|専門家|ベンダー|メーカー|銀行/, '外部専門家（税理士・社労士）'],
  ];
  const guessOwner = (t) => { for (const [re, o] of OWNER_HINTS) if (re.test(t)) return o; return OWNERS[0]; };
  const guessDue = (t) => {
    const d = new Date(); const add = (n) => { const x = new Date(d); x.setDate(x.getDate() + n); return x.toISOString().slice(0, 10); };
    if (/今週|今すぐ|即日|3日/.test(t)) return add(7);
    if (/来週|2週間|10日/.test(t)) return add(14);
    if (/今月|1ヶ月|1か月|1カ月|月内|30日/.test(t)) return add(30);
    if (/3ヶ月|3か月|四半期/.test(t)) return add(90);
    return add(14);
  };
  function parseNextSteps(md) {
    const lines = String(md || '').replace(/\r/g, '').split('\n');
    let inSec = false; const out = [];
    for (const ln of lines) {
      const t = ln.trim();
      if (/^##\s+/.test(t)) { inSec = /次の一歩|次のアクション|今週できる/.test(t); continue; }
      if (!inSec) continue;
      const m = t.match(/^(?:[-*]|\d+[.)、])\s*(?:\[[ x]\]\s*)?(.+)$/);
      if (!m) continue;
      let text = m[1].replace(/\*\*/g, '').trim();
      let owner = null, due = '';
      const paren = text.match(/[（(]\s*(?:担当|担当者)\s*[:：]\s*([^／/）)]+)(?:[／/]\s*(?:(?:期限|目安)\s*[:：]\s*)?([^）)]+))?\s*[）)]/);
      if (paren) { owner = guessOwner(paren[1]); due = guessDue(paren[2] || paren[1]); text = text.replace(paren[0], '').trim(); }
      else { const o2 = text.match(/(?:担当|担当者)\s*[:：]\s*([^／/、。]+)/); if (o2) owner = guessOwner(o2[1]); }
      text = text.replace(/[：:]\s*$/, '').replace(/^（.*?）\s*/, '').trim();
      if (text.length < 4) continue;
      out.push({ text: text.length > 120 ? text.slice(0, 118) + '…' : text, owner: owner || guessOwner(text), due: due || guessDue(text), raw: m[1] });
    }
    return out.slice(0, 8);
  }
  function NextStepsPicker({ answer, saved, onSave, toast }) {
    const parsed = useMemo(() => parseNextSteps(answer), [answer]);
    const [rows, setRows] = useState(parsed);
    const [picked, setPicked] = useState(() => new Set(parsed.map((_, i) => i)));
    useEffect(() => { setRows(parsed); setPicked(new Set(parsed.map((_, i) => i))); }, [parsed]);
    const savedTexts = new Set((saved || []).map(a => a.text.trim()));
    // 登録後：未登録の行を自動で再チェック（残りをワンクリックで登録できるように）
    const savedKey = Array.from(savedTexts).sort().join('\u0001');
    useEffect(() => { setPicked(new Set(rows.map((r, i) => savedTexts.has(r.text.trim()) ? -1 : i).filter(i => i >= 0))); }, [savedKey]);
    if (!parsed.length) return <div className="ms-note"><i className="fas fa-circle-info mr-1"></i>AI回答に「次の一歩」の箇条書きが見つかりませんでした。「詳細」タブのアクション案から保存できます。</div>;
    const n = rows.filter((r, i) => picked.has(i) && r.text.trim() && !savedTexts.has(r.text.trim())).length;
    return (
      <div className="ms-nextsteps" id="ms-nextsteps">
        <div className="ms-nextsteps-head"><i className="fas fa-bolt"></i>AIの「次の一歩」をアクションプランへ<span className="ms-nextsteps-sub">チェックした行を担当・期限つきで登録（担当・期限はAI文中の目安から推定。直せます）</span></div>
        <div className="ms-act-drafts">{rows.map((r, i) => { const dup = savedTexts.has(r.text.trim()); return (
          <div key={i} className={`ms-act-draft ${dup ? 'is-dup' : ''}`}>
            <input type="checkbox" checked={picked.has(i) && !dup} disabled={dup} onChange={() => setPicked(p => { const x = new Set(p); x.has(i) ? x.delete(i) : x.add(i); return x; })} />
            <input className="ms-input flex-1" value={r.text} onChange={(e) => setRows(rs => rs.map((x, j) => j === i ? { ...x, text: e.target.value } : x))} />
            <select className="ms-input" value={r.owner} onChange={(e) => setRows(rs => rs.map((x, j) => j === i ? { ...x, owner: e.target.value } : x))}>{OWNERS.map(o => <option key={o}>{o}</option>)}</select>
            <input type="date" className="ms-input" value={r.due} onChange={(e) => setRows(rs => rs.map((x, j) => j === i ? { ...x, due: e.target.value } : x))} />
            {dup && <Tag kind="info">登録済み</Tag>}
          </div>
        ); })}</div>
        <div className="flex gap-2 flex-wrap items-center mt-2">
          <button className="btn btn-primary" id="ms-nextsteps-save" disabled={!n} onClick={() => { const sel = rows.filter((r, i) => picked.has(i) && r.text.trim() && !savedTexts.has(r.text.trim())); if (!sel.length) { toast?.('登録する行を選んでください'); return; } onSave(sel); }}><i className="fas fa-list-check"></i>{n ? `${n}件をアクションプランに登録` : 'すべて登録済み'}</button>
          <span className="ms-free-hint">登録後は「アクション管理へ」から担当・期限・完了を管理できます（作業完了と効果検証は区別して記録）</span>
        </div>
      </div>
    );
  }

  /* ---------- 経営相談室ページ ---------- */
  function ConsultPage({ store, update, STORES, toast, initialPlanId, onOpenActions, getKpi }) {
    const [planId, setPlanId] = useState(initialPlanId || null);
    const existing = store.plans.find(p => p.id === planId) || null;
    const [themeId, setThemeId] = useState(existing?.themeId || THEMES[0].id);
    const [answers, setAnswers] = useState(existing?.answers || { interests: [], scope: 'all', storeIds: [], readiness: 'none', memo: '' });
    const [tab, setTab] = useState(existing?.consult?.answer ? 'ai' : 'tree'); // ai | tree | pdca | detail  ※切替しても answers は保持
    const [confirmDel, setConfirmDel] = useState(null);
    // 自由記入相談
    const [question, setQuestion] = useState(existing?.consult?.question || '');
    const [aiText, setAiText] = useState(existing?.consult?.answer || '');
    const [aiMeta, setAiMeta] = useState(existing?.consult?.meta || null);
    const [aiStatus, setAiStatus] = useState('idle'); // idle | running | done | error
    const [aiErr, setAiErr] = useState('');
    const [aiOk, setAiOk] = useState(null);
    const [followUp, setFollowUp] = useState('');
    const [thread, setThread] = useState(existing?.consult?.thread || []); // [{q, a, at}]
    const [autoSet, setAutoSet] = useState(null);
    const abortRef = useRef(null);
    const [consultOpen, setConsultOpen] = useState(!existing || !!existing?.consult?.question);
    useEffect(() => { fetch('/api/insight/health').then(r => r.json()).then(j => setAiOk(!!j.configured)).catch(() => setAiOk(false)); return () => { try { abortRef.current?.abort(); } catch {} }; }, []);
    const theme = THEMES.find(t => t.id === themeId) || THEMES[0];
    const storeNames = (answers.storeIds || []).map(id => STORES.find(s => s.id === id)?.name).filter(Boolean);
    const learnVer = useLearnVersion();
    const learnings = useMemo(() => selectRelevant({ themeId: theme.id, text: [theme.label, ...(answers.interests || []).map(id => theme.interests.find(i => i.id === id)?.label), answers.memo].filter(Boolean).join(' '), limit: 4 }), [theme, answers.interests.join(','), answers.memo, learnVer]);
    const guide = useMemo(() => buildGuide(theme, { ...answers, storeNames }, learnings), [theme, answers, storeNames.join('|'), learnings]);
    const planActions = store.actions.filter(a => a.planId === planId);
    const dirty = !existing || JSON.stringify(existing.answers) !== JSON.stringify(answers) || existing.themeId !== themeId || (existing.consult?.answer || '') !== aiText || (existing.consult?.question || '') !== question.trim();

    const loadPlan = (p) => { setPlanId(p.id); setThemeId(p.themeId); setAnswers(p.answers); setQuestion(p.consult?.question || ''); setAiText(p.consult?.answer || ''); setAiMeta(p.consult?.meta || null); setThread(p.consult?.thread || []); setAiStatus(p.consult?.answer ? 'done' : 'idle'); setTab(p.consult?.answer ? 'ai' : 'tree'); setConsultOpen(!!p.consult?.question); setAutoSet(null); };
    const newPlan = () => { try { abortRef.current?.abort(); } catch {} setPlanId(null); setThemeId(THEMES[0].id); setAnswers({ interests: [], scope: 'all', storeIds: [], readiness: 'none', memo: '' }); setTab('tree'); setQuestion(''); setAiText(''); setAiMeta(null); setThread([]); setAiStatus('idle'); setAiErr(''); setConsultOpen(true); setAutoSet(null); };
    const consultData = () => (question.trim() || aiText) ? { question: question.trim(), answer: aiText, meta: aiMeta, thread, at: nowISO() } : undefined;
    const savePlan = () => {
      let id = planId;
      const consult = consultData();
      update(s => {
        if (id && s.plans.find(p => p.id === id)) { s.plans = s.plans.map(p => p.id === id ? { ...p, themeId, answers, consult, updatedAt: nowISO(), guideVersion: GUIDE_VERSION } : p); }
        else { id = uid(); s.plans.unshift({ id, themeId, answers, consult, guideVersion: GUIDE_VERSION, createdAt: nowISO(), updatedAt: nowISO() }); }
        return s;
      });
      setPlanId(id); toast?.('相談プランを保存しました');
      if (learnings.length) setTimeout(() => markUsed(learnings.map(k => k.id), '経営相談室'), 0);
      return id;
    };
    const saveActions = (sel, source) => {
      const id = planId || savePlan();
      let added = 0;
      update(s => {
        sel.forEach(d => {
          const t = d.text.trim(); const ex = s.actions.find(a => a.planId === id && a.text.trim() === t);
          if (ex) { ex.owner = d.owner; if (d.due) ex.due = d.due; } else { s.actions.push({ id: uid(), planId: id, text: t, owner: d.owner, due: d.due || '', done: false, doneAt: null, createdAt: nowISO(), source: source || 'guide' }); added++; }
        });
        const p = s.plans.find(x => x.id === id); if (p) p.updatedAt = nowISO();
        return s;
      });
      toast?.(added ? `${added}件のアクションを保存しました${sel.length - added ? `（${sel.length - added}件は更新）` : ''}` : 'すべて保存済みでした（担当・期限を更新）');
    };
    const toggleDone = (aid) => update(s => { const a = s.actions.find(x => x.id === aid); if (a) { a.done = !a.done; a.doneAt = a.done ? nowISO() : null; } return s; });
    const deletePlan = (p) => update(s => { s.plans = s.plans.filter(x => x.id !== p.id); s.actions = s.actions.filter(a => a.planId !== p.id); return s; });

    /* 自由記入 → AIへ。分類JSONでテーマ・関心事・対象・準備状況を自動セット（利用者が後で直せる） */
    const interestsCatalog = useMemo(() => Object.fromEntries(THEMES.map(t => [t.id, t.interests.map(i => ({ id: i.id, label: i.label }))])), []);
    const applyMeta = (meta) => {
      if (!meta || typeof meta !== 'object') return;
      const th = THEMES.find(t => t.id === meta.theme);
      const applied = [];
      if (th) {
        const ints = Array.isArray(meta.interests) ? meta.interests.filter(id => th.interests.some(i => i.id === id)).slice(0, 3) : [];
        const sids = Array.isArray(meta.storeIds) ? meta.storeIds.filter(id => STORES.some(s => s.id === id)) : [];
        const scope = meta.scope === 'store' && sids.length ? 'store' : 'all';
        const readiness = READINESS.some(r => r.id === meta.readiness) ? meta.readiness : 'none';
        setThemeId(th.id);
        setAnswers(a => ({ ...a, interests: ints, scope, storeIds: sids, readiness, memo: a.memo || (question.trim().slice(0, 200)) }));
        applied.push(`テーマ：${th.label}`);
        if (ints.length) applied.push(`関心事：${ints.map(id => th.interests.find(i => i.id === id)?.label).join('、')}`);
        applied.push(`対象：${scope === 'all' ? '5店舗全体' : sids.map(id => STORES.find(s => s.id === id)?.name).join('・')}`);
        applied.push(`判断材料：${READINESS.find(r => r.id === readiness)?.label}`);
      }
      setAutoSet(applied);
    };
    const runConsult = async (fu) => {
      const q = (fu || question).trim();
      if (q.length < 5) { toast?.('相談内容を5文字以上入力してください'); return; }
      try { abortRef.current?.abort(); } catch {}
      const ac = new AbortController(); abortRef.current = ac;
      const history = fu ? [...thread.flatMap(t => [{ role: 'user', content: t.q }, { role: 'assistant', content: t.a }]), { role: 'user', content: question.trim() }, { role: 'assistant', content: aiText }] : [];
      if (fu) { setThread(th => [...th, { q: question.trim(), a: aiText, at: nowISO() }]); setQuestion(fu); setFollowUp(''); }
      setAiStatus('running'); setAiErr(''); setAiText(''); setTab('ai'); if (!fu) setAiMeta(null);
      const kpi = (() => { try { return getKpi?.() || {}; } catch { return {}; } })();
      const knowledge = selectRelevant({ text: q, limit: 5 });
      try {
        const r = await streamConsult({ question: q, kpi, stores: STORES.map(s => s.name), knowledge: knowledge.map(k => ({ id: k.id, title: k.title, source: k.source, date: k.date, themes: k.themes, tags: k.tags, principles: k.principles, numbers: k.numbers, checklist: k.checklist, applications: k.applications, weight: k.weight })), history, interestsCatalog, signal: ac.signal,
          onMeta: (meta) => { if (!fu) { setAiMeta(meta); applyMeta(meta); } }, onText: (t) => setAiText(t) });
        if (knowledge.length) { window.__ipbLastKnowledge = knowledge; setTimeout(() => markUsed(knowledge.map(k => k.id), '経営相談室AI'), 0); }
        setAiStatus('done');
        if (!r.text) throw new Error('応答が空でした');
      } catch (e) { if (e?.name === 'AbortError') { setAiStatus('idle'); return; } setAiErr(String(e.message || e)); setAiStatus('error'); }
    };
    const stopConsult = () => { try { abortRef.current?.abort(); } catch {} setAiStatus(aiText ? 'done' : 'idle'); };
    const consultToMarkdown = () => [`# 経営相談（自由記入）`, '', ...thread.flatMap(t => [`## 相談`, t.q, '', `## AIの整理`, t.a, '']), `## 相談`, question.trim(), '', `## AIの整理`, aiText, '', '> ※ AIによる仮説・論点整理です。最終判断は確認条件を満たしたうえで。'].join('\n');
    const EXAMPLES = [
      '南ヶ丘病院店の処方箋が前年比で減っている。門前依存が怖いので、面対応や在宅に軏を切るべきか迷っている',
      '来期の賃上げをどの水準にするか。調剤ベースアップ評価料でどこまで補えるかを知りたい',
      '調剤機器（監査システム）を2店舗に入れたいが、金利が上がっている中で借入していいか',
      '薬剤師の離職が続いている。店舗間の応援体制と採用をどう組み立てるか',
    ];

    return (
      <div className="ms-page" id="ms-consult">
        {confirmDel && <ConfirmDialog title="相談プランを削除" body={`「${THEMES.find(t => t.id === confirmDel.themeId)?.label}」のプランと、紐づくアクション ${store.actions.filter(a => a.planId === confirmDel.id).length} 件を削除します。元に戻せません。`} onOk={() => { deletePlan(confirmDel); if (confirmDel.id === planId) newPlan(); setConfirmDel(null); toast?.('削除しました'); }} onCancel={() => setConfirmDel(null)} />}
        <div className="yoy-page-head ms-head">
          <div><div className="yoy-page-title"><i className="fas fa-comments mr-2"></i>経営相談室</div>
            <div className="yoy-page-sub">まず自由に相談を書くと、AIが論点を整理して初回回答し、テーマ・関心事を自動セット。続けて定型ガイド（仮説・判断材料・選択肢・アクション案）で判断を固め、アクションへつなげます。</div></div>
          <div className="flex gap-2 flex-wrap no-print">
            <button className="btn btn-secondary" onClick={newPlan}><i className="fas fa-plus"></i>新しい相談</button>
            <button className="btn btn-secondary" onClick={() => copyText((aiText ? consultToMarkdown() + '\n\n---\n\n' : '') + planToMarkdown(theme, guide, existing || { createdAt: nowISO(), updatedAt: nowISO() }, planActions), toast, 'Markdownをコピーしました')}><i className="fas fa-copy"></i>MD</button>
            <button className="btn btn-primary" id="ms-save-plan" onClick={savePlan} disabled={!dirty && !!existing}><i className="fas fa-floppy-disk"></i>{existing ? (dirty ? 'プランを更新' : '保存済み') : 'プランを保存'}</button>
          </div>
        </div>

        {/* 自由記入で相談（最初の入口） */}
        <section className={`dash-card ms-free ${consultOpen ? '' : 'is-collapsed'}`} id="ms-free">
          <div className="ms-free-head" onClick={() => setConsultOpen(o => !o)} role="button">
            <div className="dash-card-title"><span className="badge">0</span>まずは自由に相談する<Tag kind="learn">AI</Tag></div>
            <div className="ms-free-sub">気になっていることをそのまま書いてください。AIが論点を整理して初回回答を返し、下のテーマ・関心事・対象店舗を自動でセットします（学習ノートの学びも参照）。</div>
            <i className={`fas fa-chevron-${consultOpen ? 'up' : 'down'} ms-free-chev`}></i>
          </div>
          {consultOpen && (
            <div className="ms-free-body">
              <textarea className="ms-input ms-free-text" id="ms-free-q" rows={4} value={question} onChange={(e) => setQuestion(e.target.value)} placeholder={'例）' + EXAMPLES[0]} disabled={aiStatus === 'running'} onKeyDown={(e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') runConsult(); }} />
              <div className="ms-free-examples">{EXAMPLES.map((ex, i) => <button key={i} type="button" className="ms-free-ex" onClick={() => setQuestion(ex)} disabled={aiStatus === 'running'}><i className="fas fa-lightbulb"></i>{ex.slice(0, 26)}…</button>)}</div>
              <div className="flex gap-2 flex-wrap items-center mt-2">
                {aiStatus === 'running'
                  ? <button className="btn btn-secondary" onClick={stopConsult}><i className="fas fa-stop"></i>停止</button>
                  : <button className="btn btn-primary" id="ms-free-run" onClick={() => runConsult()} disabled={question.trim().length < 5 || aiOk === false}><i className="fas fa-paper-plane"></i>{aiText ? 'AIにもう一度相談する' : 'AIに相談する'}</button>}
                <span className="ms-free-hint">{aiOk === false ? 'AIが未設定のため、下のテーマ選択から定型ガイドをご利用ください' : 'Ctrl/⌘ + Enter で送信。回答は「AIの整理」タブに表示。最終判断はご自身で'}</span>
              </div>
              {autoSet && autoSet.length > 0 && <div className="ms-free-autoset" id="ms-free-autoset"><i className="fas fa-wand-magic-sparkles"></i>AIが相談文から自動セット：{autoSet.join('／')}<span>（下の「相談内容」で直せます）</span></div>}
              {aiErr && <div className="ms-caveat is-error mt-2"><i className="fas fa-triangle-exclamation"></i>AI相談に失敗：{aiErr}。テーマを選んで定型ガイドをご利用ください。</div>}
            </div>
          )}
        </section>

        <div className="ms-layout">
          {/* 左：入力 */}
          <aside className="dash-card ms-form">
            <div className="dash-card-title"><span className="badge">1</span>相談内容{autoSet && <Tag kind="learn">AIが自動セット</Tag>}</div>
            <label className="ms-label">相談テーマ</label>
            <div className="ms-theme-grid">{THEMES.map(t => <button key={t.id} type="button" className={`ms-theme ${themeId === t.id ? 'is-on' : ''}`} onClick={() => { setThemeId(t.id); setAnswers(a => ({ ...a, interests: [] })); }}><i className={`fas ${t.icon}`}></i><span>{t.label}</span></button>)}</div>
            <div className="ms-theme-desc">{theme.desc}</div>
            <label className="ms-label">関心事（複数可）</label>
            <div className="ms-chips">{theme.interests.map(i => <label key={i.id} className={`ms-chip ${answers.interests.includes(i.id) ? 'is-on' : ''}`}><input type="checkbox" checked={answers.interests.includes(i.id)} onChange={() => setAnswers(a => ({ ...a, interests: a.interests.includes(i.id) ? a.interests.filter(x => x !== i.id) : [...a.interests, i.id] }))} />{i.label}</label>)}</div>
            <label className="ms-label">対象範囲</label>
            <div className="ms-radio-row">
              <label className={`ms-chip ${answers.scope === 'all' ? 'is-on' : ''}`}><input type="radio" name="ms-scope" checked={answers.scope === 'all'} onChange={() => setAnswers(a => ({ ...a, scope: 'all' }))} />5店舗全体</label>
              <label className={`ms-chip ${answers.scope === 'store' ? 'is-on' : ''}`}><input type="radio" name="ms-scope" checked={answers.scope === 'store'} onChange={() => setAnswers(a => ({ ...a, scope: 'store' }))} />個別店舗</label>
            </div>
            {answers.scope === 'store' && <div className="ms-chips">{STORES.map(s => <label key={s.id} className={`ms-chip ${answers.storeIds.includes(s.id) ? 'is-on' : ''}`}><input type="checkbox" checked={answers.storeIds.includes(s.id)} onChange={() => setAnswers(a => ({ ...a, storeIds: a.storeIds.includes(s.id) ? a.storeIds.filter(x => x !== s.id) : [...a.storeIds, s.id] }))} />{s.name}</label>)}</div>}
            <label className="ms-label">判断材料の準備状況</label>
            <div className="ms-radio-col">{READINESS.map(r => <label key={r.id} className={`ms-chip ${answers.readiness === r.id ? 'is-on' : ''}`}><input type="radio" name="ms-ready" checked={answers.readiness === r.id} onChange={() => setAnswers(a => ({ ...a, readiness: r.id }))} />{r.label}</label>)}</div>
            <label className="ms-label">メモ（任意）</label>
            <textarea className="ms-input w-full" rows={3} placeholder="背景・気になっていること" value={answers.memo} onChange={(e) => setAnswers(a => ({ ...a, memo: e.target.value }))}></textarea>
            {dirty && <div className="ms-note mt-2"><i className="fas fa-pen mr-1"></i>未保存の変更があります（表示切替では消えません）</div>}

            {store.plans.length > 0 && (
              <div className="mt-4"><div className="dash-card-title"><span className="badge">2</span>保存済みの相談（{store.plans.length}）</div>
                <ul className="ms-plan-list">{store.plans.map(p => { const t = THEMES.find(x => x.id === p.themeId); const n = store.actions.filter(a => a.planId === p.id); return (
                  <li key={p.id} className={p.id === planId ? 'is-on' : ''}>
                    <button type="button" className="ms-plan-btn" onClick={() => loadPlan(p)}><i className={`fas ${p.consult?.answer ? 'fa-robot' : (t?.icon || 'fa-file')}`}></i><span>{p.consult?.meta?.title || t?.label}</span><small>{p.consult?.meta?.title ? `${t?.label}・` : ''}{fmtDate(p.updatedAt)}・アクション {n.filter(a => a.done).length}/{n.length}</small></button>
                    <button type="button" className="btn-mini" title="削除" onClick={() => setConfirmDel(p)}><i className="fas fa-trash"></i></button>
                  </li>
                ); })}</ul>
              </div>
            )}
          </aside>

          {/* 右：結果 */}
          <main className="dash-card ms-result">
            <div className="ms-tabs no-print" role="tablist">
              {[...((aiText || aiStatus === 'running') ? [['ai', 'fa-robot', 'AIの整理']] : []), ['tree', 'fa-sitemap', 'ツリー'], ['pdca', 'fa-rotate', 'PDCA'], ['detail', 'fa-list', '詳細']].map(([id, ic, lb]) => <button key={id} role="tab" aria-selected={tab === id} className={`ms-tab ${tab === id ? 'is-on' : ''}`} id={`ms-tab-${id}`} onClick={() => setTab(id)}><i className={`fas ${ic}`}></i>{lb}</button>)}
              <div className="flex-1"></div>
              {planId && <button className="btn btn-secondary" onClick={() => onOpenActions?.(planId)}><i className="fas fa-list-check"></i>アクション管理へ</button>}
            </div>
            {tab === 'ai' && (
              <div className="ms-ai" id="ms-ai">
                {thread.length > 0 && <details className="ms-ai-thread"><summary>これまでのやりとり（{thread.length}往復）</summary>{thread.map((t, i) => <div key={i} className="ms-ai-past"><div className="ms-ai-q"><i className="fas fa-user"></i>{t.q}</div><div className="ms-ai-a">{renderConsultMd(t.a)}</div></div>)}</details>}
                <div className="ms-ai-q"><i className="fas fa-user"></i>{question.trim() || '（相談文なし）'}</div>
                <div className="ms-ai-meta"><i className="fas fa-robot"></i>AIの論点整理 {aiStatus === 'running' ? <span className="ms-ai-live">生成中…</span> : aiStatus === 'done' ? '完了' : ''}{aiMeta?.title && <span className="ms-ai-title">「{aiMeta.title}」</span>}</div>
                <div className="ms-ai-body">{aiText ? renderConsultMd(aiText) : (aiStatus === 'running' ? <div className="ms-note">考えています…（自社KPIと学習ノートを照らし合わせ中）</div> : null)}{aiStatus === 'running' && <span className="ai-cursor inline-block">▍</span>}</div>
                {aiStatus === 'done' && (
                  <div className="ms-ai-foot">
                    <NextStepsPicker answer={aiText} saved={planActions} toast={toast} onSave={(sel) => { saveActions(sel, '経営相談室AI'); }} />
                    <div className="ms-caveat"><i className="fas fa-circle-info"></i>AIによる仮説・論点整理です。「ツリー」「詳細」タブの定型ガイドと合わせて確認してください。定型ガイドのアクション案は「詳細」タブから保存できます。</div>
                    <div className="ms-ai-follow">
                      <input className="ms-input flex-1" id="ms-ai-follow" placeholder="続けて聞く（例：選択肢Bを選ぶ場合の最初の3ヶ月は？）" value={followUp} onChange={(e) => setFollowUp(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' && followUp.trim().length >= 5) runConsult(followUp); }} />
                      <button className="btn btn-secondary" onClick={() => runConsult(followUp)} disabled={followUp.trim().length < 5}><i className="fas fa-reply"></i>続けて聞く</button>
                      <button className="btn btn-secondary" onClick={() => setTab('tree')}><i className="fas fa-sitemap"></i>定型ガイドを見る</button>
                      <button className="btn btn-secondary" onClick={() => copyText(consultToMarkdown(), toast, 'AIの整理をコピーしました')}><i className="fas fa-copy"></i>MD</button>
                    </div>
                  </div>
                )}
              </div>
            )}
            {tab !== 'ai' && guide.limited && <div className="ms-caveat ms-caveat-top"><i className="fas fa-circle-info"></i>{guide.caveat}</div>}
            {tab === 'tree' && <TreeView theme={theme} guide={guide} plan={existing} actions={planActions} />}
            {tab === 'pdca' && <PdcaView theme={theme} guide={guide} actions={planActions} />}
            {tab === 'detail' && <DetailView theme={theme} guide={guide} plan={existing} actions={planActions} onSaveActions={saveActions} onToggleDone={toggleDone} toast={toast} />}
          </main>
        </div>
      </div>
    );
  }

  /* ---------- アクションプラン管理 ---------- */
  function ActionsPage({ store, update, toast, onOpenPlan, focusPlanId }) {
    const [filter, setFilter] = useState('open'); // open | done | all
    const [planF, setPlanF] = useState(focusPlanId || 'all');
    const [confirmDel, setConfirmDel] = useState(null);
    const [edit, setEdit] = useState(null);
    const list = store.actions.filter(a => (planF === 'all' || a.planId === planF) && (filter === 'all' || (filter === 'done') === !!a.done))
      .slice().sort((a, b) => (a.done - b.done) || String(a.due || '9999').localeCompare(String(b.due || '9999')));
    const themeOf = (pid) => THEMES.find(t => t.id === store.plans.find(p => p.id === pid)?.themeId);
    const overdue = (a) => !a.done && a.due && a.due < todayStr();
    const saveEdit = () => { update(s => { const a = s.actions.find(x => x.id === edit.id); if (a) Object.assign(a, { text: edit.text.trim() || a.text, owner: edit.owner, due: edit.due }); return s; }); setEdit(null); toast?.('更新しました'); };
    const md = () => {
      const L = ['# アクションプラン一覧', `- 出力日: ${fmtDate(nowISO())}`, ''];
      store.plans.forEach(p => { const t = THEMES.find(x => x.id === p.themeId); const as = store.actions.filter(a => a.planId === p.id); if (!as.length) return; L.push(`## ${t?.label}（更新 ${fmtDate(p.updatedAt)}）`); as.forEach(a => L.push(`- [${a.done ? 'x' : ' '}] ${a.text}（${a.owner || '未定'}／${a.due ? fmtDate(a.due) : '期限未定'}）${a.done ? ' ※作業完了・効果は未検証' : ''}`)); L.push(''); });
      return L.join('\n');
    };
    return (
      <div className="ms-page" id="ms-actions">
        {confirmDel && <ConfirmDialog title="アクションを削除" body={`「${confirmDel.text}」を削除します。元に戻せません。`} onOk={() => { update(s => { s.actions = s.actions.filter(a => a.id !== confirmDel.id); return s; }); setConfirmDel(null); toast?.('削除しました'); }} onCancel={() => setConfirmDel(null)} />}
        {edit && (
          <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) setEdit(null); }}>
            <div className="modal-card ms-confirm"><div className="modal-head"><div className="modal-title"><i className="fas fa-pen mr-2"></i>アクションを編集</div><button className="modal-close" onClick={() => setEdit(null)}><i className="fas fa-xmark"></i></button></div>
              <div className="modal-body ms-edit">
                <label className="ms-label">内容</label><input className="ms-input w-full" value={edit.text} onChange={(e) => setEdit({ ...edit, text: e.target.value })} />
                <label className="ms-label">担当</label><select className="ms-input w-full" value={edit.owner} onChange={(e) => setEdit({ ...edit, owner: e.target.value })}>{OWNERS.map(o => <option key={o}>{o}</option>)}</select>
                <label className="ms-label">期限</label><input type="date" className="ms-input w-full" value={edit.due || ''} onChange={(e) => setEdit({ ...edit, due: e.target.value })} />
                <div className="flex gap-2 justify-end mt-3"><button className="btn btn-secondary" onClick={() => setEdit(null)}>キャンセル</button><button className="btn btn-primary" id="ms-edit-save" onClick={saveEdit}>保存</button></div>
              </div></div>
          </div>
        )}
        <div className="yoy-page-head ms-head">
          <div><div className="yoy-page-title"><i className="fas fa-list-check mr-2"></i>アクションプラン</div><div className="yoy-page-sub">相談で決めた行動の担当・期限・完了状況。「完了」は作業の完了であり、施策の効果確認ではありません。担当者への通知は行いません。</div></div>
          <div className="flex gap-2 flex-wrap no-print"><button className="btn btn-secondary" onClick={() => copyText(md(), toast, 'Markdownをコピーしました')}><i className="fas fa-copy"></i>MDコピー</button><button className="btn btn-secondary" onClick={() => downloadText(`IPB_actions_${todayStr()}.md`, md())}><i className="fas fa-download"></i>MD保存</button></div>
        </div>
        <div className="dash-card">
          <div className="flex gap-2 flex-wrap items-center mb-3 no-print">
            {[['open', '未完了'], ['done', '作業完了'], ['all', 'すべて']].map(([id, lb]) => <button key={id} className={`news-filter ${filter === id ? 'is-on' : ''}`} onClick={() => setFilter(id)}>{lb}<span>{store.actions.filter(a => id === 'all' || (id === 'done') === !!a.done).length}</span></button>)}
            <select className="ms-input" value={planF} onChange={(e) => setPlanF(e.target.value)}><option value="all">すべての相談</option>{store.plans.map(p => <option key={p.id} value={p.id}>{THEMES.find(t => t.id === p.themeId)?.label}（{fmtDate(p.updatedAt)}）</option>)}</select>
          </div>
          {!store.actions.length ? <Empty icon="fa-list-check" title="アクションはまだありません" sub="経営相談室の「詳細」タブでアクション案を保存すると、ここに表示されます。" />
            : !list.length ? <Empty title="この条件のアクションはありません" />
            : (
              <ul className="ms-act-list">{list.map(a => { const t = themeOf(a.planId); return (
                <li key={a.id} className={`ms-act ${a.done ? 'is-done' : ''} ${overdue(a) ? 'is-overdue' : ''}`}>
                  <button type="button" className="ms-check" title={a.done ? '未完了に戻す' : '作業完了にする'} onClick={() => update(s => { const x = s.actions.find(y => y.id === a.id); if (x) { x.done = !x.done; x.doneAt = x.done ? nowISO() : null; } return s; })}><i className={`fas ${a.done ? 'fa-square-check' : 'fa-square'}`}></i></button>
                  <div className="min-w-0 flex-1">
                    <div className="ms-act-text">{a.source === '経営相談室AI' && <Tag kind="learn">AI提案</Tag>}{a.text}</div>
                    <div className="ms-act-meta"><button type="button" className="ms-link" onClick={() => onOpenPlan?.(a.planId)}><i className={`fas ${t?.icon || 'fa-file'}`}></i>{t?.label || '（相談なし）'}</button><span><i className="fas fa-user"></i>{a.owner || '担当未定'}</span><span className={overdue(a) ? 'ms-overdue' : ''}><i className="fas fa-calendar"></i>{a.due ? fmtDate(a.due) : '期限未定'}{overdue(a) ? '（期限超過）' : ''}</span>{a.done && <Tag kind="done">作業完了 {fmtDate(a.doneAt)}</Tag>}{a.done && <Tag kind="unverified">効果は未検証</Tag>}</div>
                  </div>
                  <div className="ms-act-btns"><button className="btn-mini" title="編集" onClick={() => setEdit({ ...a })}><i className="fas fa-pen"></i></button><button className="btn-mini" title="削除" onClick={() => setConfirmDel(a)}><i className="fas fa-trash"></i></button></div>
                </li>
              ); })}</ul>
            )}
        </div>
      </div>
    );
  }

  /* ---------- レポート ---------- */
  const REPORT_KINDS = [
    { id: 'daily',   label: 'デイリー', icon: 'fa-sun',           template: `## 3行サマリー\n- \n- \n- \n\n## 注目トピック（重要度4以上・最大5件）\n| 重要度 | トピック | 日本経済への影響 | 5店舗へのヒント | 出典 |\n|---|---|---|---|---|\n| 4 |  |  |  |  |\n\n## 数字\n- \n\n## 助言\n- \n\n## その他の記事\n- ` },
    { id: 'weekly',  label: '週次',     icon: 'fa-calendar-week', template: `## 今週の流れ\n- \n\n## 来週の確認事項\n- [ ] \n\n## 考えるべき問い\n- ` },
    { id: 'monthly', label: '月次',     icon: 'fa-calendar',      template: `## 外部環境\n### 日本経済（景気・物価・賃金・雇用）\n- \n### 金融・市場（日銀・金利・為替・株価）\n- \n### 財政・海外・地域（税制・予算・海外需要・北陸/石川）\n- \n### 薬局経営（制度・地域連携・人材・収益・店舗運営）\n- \n\n## 自社への影響\n- \n\n## 事業計画・幹部会の検討事項\n- ` },
  ];
  const FIELDS = [
    { id: 'economy', label: '日本経済', themes: '景気・GDP・個人消費・物価・賃金・雇用' },
    { id: 'finance', label: '金融・市場', themes: '日銀・金利・為替・株価' },
    { id: 'fiscal',  label: '財政・海外・地域', themes: '税制・予算・海外需要・北陸/石川の経済' },
    { id: 'pharma',  label: '薬局経営', themes: '制度・地域連携・人材・収益・店舗運営' },
  ];
  const DEMO_REPORTS = [
    { id: 'demo-daily', kind: 'daily', date: '2026-10-01', title: '【デモ】デイリーブリーフィングの表示例', isDemo: true, source: 'デモデータ（実際のニュースではありません）', fields: ['economy', 'finance', 'pharma'],
      body: `## 3行サマリー\n- （デモ）金利・物価の動向に関する見出しが複数。\n- （デモ）薬価・調剤報酬に関する論点提示の報道。\n- （デモ）北陸の景況感に関する地域報道。\n\n## 注目トピック（重要度4以上・最大5件）\n| 重要度 | トピック | 日本経済への影響 | 5店舗へのヒント | 出典 |\n|---|---|---|---|---|\n| 5 | （デモ）長期金利の上昇 | 借入コスト・設備投資判断に影響 | 変動金利借入の確認、投資の優先順位付け | デモ |\n| 4 | （デモ）薬価改定の論点提示 | 医療費抑制の方向性 | 門前店の薬価差益への影響を試算 | デモ |\n\n## 数字\n- （デモ）数値はここに記載します。\n\n## 助言\n- （デモ）「判断前に確認したい情報」を揃えてから結論を出す。\n\n## その他の記事\n- （デモ）` },
    { id: 'demo-weekly', kind: 'weekly', date: '2026-10-03', title: '【デモ】週次レポートの表示例', isDemo: true, source: 'デモデータ', fields: ['economy', 'pharma'],
      body: `## 今週の流れ\n- （デモ）金利上昇観測と物価上振れが続いた週。\n\n## 来週の確認事項\n- [ ] （デモ）借入一覧の整理\n- [ ] （デモ）評価料の要件確認\n\n## 考えるべき問い\n- （デモ）金利が1%上がったら、来期の投資計画はどう変わるか？` },
  ];

  function reportToMarkdown(r) { return `# ${r.title}\n\n- 種別: ${REPORT_KINDS.find(k => k.id === r.kind)?.label}／対象日: ${r.date}${r.isDemo ? '／**デモデータ**' : ''}\n- 分野: ${(r.fields || []).map(f => FIELDS.find(x => x.id === f)?.label).filter(Boolean).join('、') || '—'}\n- 出典: ${r.source || '—'}\n\n${r.body}`; }
  function reportToBullets(r) { return r.body.split('\n').filter(l => /^\s*[-*]\s+|^#{2,3}\s+/.test(l)).map(l => l.replace(/^#{2,3}\s+/, '■ ').replace(/^\s*[-*]\s+\[[ x]\]\s*/, '・').replace(/^\s*[-*]\s+/, '・')).join('\n'); }
  /* 軽量 Markdown → React（見出し/箇条書き/チェック/表/太字） */
  function renderMd(md) {
    const lines = String(md || '').replace(/\r/g, '').split('\n'); const out = []; let list = null; let table = null;
    const cite = (t, k) => (typeof window.renderLearnCites === 'function') ? window.renderLearnCites(t, k) : t;
    const inline = (t, k) => String(t).split(/(\*\*[^*]+\*\*)/g).filter(Boolean).map((p, i) => /^\*\*[^*]+\*\*$/.test(p) ? <b key={`${k}-${i}`}>{cite(p.slice(2, -2), `${k}-${i}b`)}</b> : <React.Fragment key={`${k}-${i}`}>{cite(p, `${k}-${i}`)}</React.Fragment>);
    const flush = () => { if (list) { out.push(<ul key={`l${out.length}`} className="ms-md-ul">{list}</ul>); list = null; } if (table) { const [h, ...b] = table; out.push(<div key={`t${out.length}`} className="ms-md-table-wrap"><table className="ms-md-table"><thead><tr>{h.map((c, j) => <th key={j}>{inline(c, j)}</th>)}</tr></thead><tbody>{b.map((r, ri) => <tr key={ri}>{r.map((c, j) => <td key={j}>{inline(c, `${ri}-${j}`)}</td>)}</tr>)}</tbody></table></div>); table = null; } };
    lines.forEach((ln, i) => {
      const t = ln.trim(); if (!t) { flush(); return; }
      if (/^\|.*\|$/.test(t)) { if (/^\|[\s:\-|]+\|$/.test(t)) return; if (list) flush(); table = table || []; table.push(t.slice(1, -1).split('|').map(c => c.trim())); return; } else if (table) flush();
      let m;
      if ((m = t.match(/^##\s+(.+)$/))) { flush(); out.push(<h4 key={i} className="ms-md-h2">{inline(m[1], i)}</h4>); return; }
      if ((m = t.match(/^###\s+(.+)$/))) { flush(); out.push(<h5 key={i} className="ms-md-h3">{inline(m[1], i)}</h5>); return; }
      if ((m = t.match(/^[-*]\s*\[( |x)\]\s*(.*)$/))) { list = list || []; list.push(<li key={i} className="is-check"><i className={`fas ${m[1] === 'x' ? 'fa-square-check' : 'fa-square'}`}></i>{inline(m[2], i)}</li>); return; }
      if ((m = t.match(/^[-*]\s+(.*)$/))) { list = list || []; list.push(<li key={i}>{inline(m[1], i)}</li>); return; }
      flush(); out.push(<p key={i} className="ms-md-p">{inline(t, i)}</p>);
    });
    flush(); return out;
  }

  function ReportsPage({ store, update, toast, mode /* 'daily' | 'periodic' | 'archive' */ }) {
    const [q, setQ] = useState('');
    const [kindF, setKindF] = useState(mode === 'daily' ? 'daily' : mode === 'periodic' ? 'weekly' : 'all');
    const [monthF, setMonthF] = useState('all');
    const [sort, setSort] = useState('desc');
    const [sel, setSel] = useState(null);
    const [editing, setEditing] = useState(null);
    const [showDemo, setShowDemo] = useState(false);
    const [confirmDel, setConfirmDel] = useState(null);
    const [preview, setPreview] = useState(null); // {report, channel}
    const [bmOnly, setBmOnly] = useState(false);
    useEffect(() => { setKindF(mode === 'daily' ? 'daily' : mode === 'periodic' ? 'weekly' : 'all'); setSel(null); }, [mode]);

    const all = useMemo(() => [...store.reports, ...(showDemo ? DEMO_REPORTS : [])], [store.reports, showDemo]);
    const months = useMemo(() => [...new Set(all.map(r => monthStr(r.date)))].sort().reverse(), [all]);
    const isBm = (id) => store.bookmarks.some(b => b.refId === id);
    const list = useMemo(() => {
      const needle = q.trim().toLowerCase();
      return all.filter(r => (kindF === 'all' || r.kind === kindF || (mode === 'periodic' && kindF === 'weekly+monthly' && r.kind !== 'daily')) && (monthF === 'all' || monthStr(r.date) === monthF) && (!bmOnly || isBm(r.id)) && (!needle || `${r.title} ${r.body} ${r.source || ''}`.toLowerCase().includes(needle)))
        .sort((a, b) => sort === 'desc' ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date));
    }, [all, q, kindF, monthF, sort, bmOnly, store.bookmarks]);
    const current = sel ? all.find(r => r.id === sel) : null;

    const startNew = (kind) => setEditing({ id: null, kind, date: todayStr(), title: '', body: REPORT_KINDS.find(k => k.id === kind).template, source: '', fields: [], isDemo: false });
    const saveReport = () => {
      if (!editing.title.trim()) { toast?.('件名を入力してください'); return; }
      if (!/^\d{4}-\d{2}-\d{2}$/.test(editing.date)) { toast?.('対象日を入力してください'); return; }
      let id = editing.id;
      update(s => { if (id) s.reports = s.reports.map(r => r.id === id ? { ...r, ...editing, updatedAt: nowISO() } : r); else { id = uid(); s.reports.unshift({ ...editing, id, createdAt: nowISO(), updatedAt: nowISO() }); } return s; });
      setEditing(null); setSel(id); toast?.('レポートを保存しました');
    };
    const toggleBm = (r) => update(s => { if (s.bookmarks.some(b => b.refId === r.id)) s.bookmarks = s.bookmarks.filter(b => b.refId !== r.id); else s.bookmarks.unshift({ id: uid(), type: 'report', refId: r.id, title: r.title, createdAt: nowISO() }); return s; });
    const title = mode === 'daily' ? 'デイリーブリーフィング' : mode === 'periodic' ? '週次・月次レポート' : 'アーカイブ';
    const printReport = (r) => { const w = window.open('', '_blank'); if (!w) { alert('ポップアップがブロックされました'); return; } w.document.write(`<!doctype html><html lang="ja"><head><meta charset="utf-8"><title>${r.title}</title><style>body{font-family:"Noto Sans JP",system-ui,sans-serif;max-width:800px;margin:32px auto;padding:0 20px;color:#1f2937;line-height:1.7}h1{font-size:20px;border-bottom:2px solid #0E2D52;padding-bottom:6px}h2{font-size:15px;margin-top:20px;border-left:4px solid #C8A85A;padding-left:8px}h3{font-size:13.5px}table{border-collapse:collapse;width:100%;font-size:12px}td,th{border:1px solid #ddd;padding:4px 8px}.meta{color:#6b7280;font-size:12px}.demo{background:#fff7e6;border:1px solid #f59e0b;padding:6px 10px;font-size:12px}</style></head><body><h1>${r.title}</h1><div class="meta">${REPORT_KINDS.find(k => k.id === r.kind)?.label}／${r.date}／出典：${r.source || '—'}</div>${r.isDemo ? '<div class="demo">デモデータ（実際のニュースではありません）</div>' : ''}<div id="c"></div><script>document.getElementById('c').innerHTML=${JSON.stringify(mdToHtml(r.body))};window.onload=()=>setTimeout(()=>window.print(),300);</script></body></html>`); w.document.close(); };

    return (
      <div className="ms-page" id={`ms-reports-${mode}`}>
        {confirmDel && <ConfirmDialog title="レポートを削除" body={`「${confirmDel.title}」を削除します。元に戻せません。`} onOk={() => { update(s => { s.reports = s.reports.filter(r => r.id !== confirmDel.id); s.bookmarks = s.bookmarks.filter(b => b.refId !== confirmDel.id); return s; }); if (sel === confirmDel.id) setSel(null); setConfirmDel(null); toast?.('削除しました'); }} onCancel={() => setConfirmDel(null)} />}
        {preview && <DeliveryPreview report={preview.report} settings={store.settings} onChangeSettings={(st) => update(s => { s.settings = { ...s.settings, ...st }; return s; })} onClose={() => setPreview(null)} toast={toast} />}
        {editing && (
          <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) { if (confirm('編集内容を破棄しますか？')) setEditing(null); } }}>
            <div className="modal-card ms-report-edit"><div className="modal-head"><div className="modal-title"><i className="fas fa-pen mr-2"></i>{editing.id ? 'レポートを編集' : 'レポートを登録'}</div><button className="modal-close" onClick={() => { if (confirm('編集内容を破棄しますか？')) setEditing(null); }}><i className="fas fa-xmark"></i></button></div>
              <div className="modal-body ms-edit">
                <div className="ms-grid2">
                  <div><label className="ms-label">種別</label><select className="ms-input w-full" value={editing.kind} onChange={(e) => setEditing({ ...editing, kind: e.target.value })}>{REPORT_KINDS.map(k => <option key={k.id} value={k.id}>{k.label}</option>)}</select></div>
                  <div><label className="ms-label">対象日</label><input type="date" className="ms-input w-full" value={editing.date} onChange={(e) => setEditing({ ...editing, date: e.target.value })} /></div>
                </div>
                <label className="ms-label">件名</label><input className="ms-input w-full" id="ms-report-title" value={editing.title} onChange={(e) => setEditing({ ...editing, title: e.target.value })} placeholder="例：10月第1週 デイリーブリーフィング" />
                <label className="ms-label">分野</label><div className="ms-chips">{FIELDS.map(f => <label key={f.id} className={`ms-chip ${editing.fields.includes(f.id) ? 'is-on' : ''}`} title={f.themes}><input type="checkbox" checked={editing.fields.includes(f.id)} onChange={() => setEditing({ ...editing, fields: editing.fields.includes(f.id) ? editing.fields.filter(x => x !== f.id) : [...editing.fields, f.id] })} />{f.label}</label>)}</div>
                <label className="ms-label">本文（Markdown）　<button type="button" className="ms-link" onClick={() => setEditing({ ...editing, body: REPORT_KINDS.find(k => k.id === editing.kind).template })}>テンプレートを挿入</button></label>
                <textarea className="ms-input w-full ms-mono" rows={14} value={editing.body} onChange={(e) => setEditing({ ...editing, body: e.target.value })}></textarea>
                <label className="ms-label">出典情報</label><input className="ms-input w-full" value={editing.source} onChange={(e) => setEditing({ ...editing, source: e.target.value })} placeholder="媒体名・URL・取得日など" />
                <label className="ms-chip mt-2"><input type="checkbox" checked={!!editing.isDemo} onChange={(e) => setEditing({ ...editing, isDemo: e.target.checked })} />デモデータとして区別する（実際のニュースではない）</label>
                <div className="flex gap-2 justify-end mt-3"><button className="btn btn-secondary" onClick={() => { if (confirm('編集内容を破棄しますか？')) setEditing(null); }}>キャンセル</button><button className="btn btn-primary" id="ms-report-save" onClick={saveReport}><i className="fas fa-floppy-disk"></i>保存</button></div>
              </div></div>
          </div>
        )}
        <div className="yoy-page-head ms-head">
          <div><div className="yoy-page-title"><i className="fas fa-newspaper mr-2"></i>{title}</div><div className="yoy-page-sub">日本経済・薬局業界のレポートを登録・閲覧します。自動取得・自動生成・実配信は行いません（登録は手動）。「日本経済への影響」と「5店舗へのヒント」を分けて記載します。</div></div>
          <div className="flex gap-2 flex-wrap no-print">
            {REPORT_KINDS.filter(k => mode === 'archive' || (mode === 'daily' ? k.id === 'daily' : k.id !== 'daily')).map(k => <button key={k.id} className="btn btn-primary" id={`ms-new-${k.id}`} onClick={() => startNew(k.id)}><i className={`fas ${k.icon}`}></i>{k.label}を登録</button>)}
          </div>
        </div>
        <div className="ms-layout ms-layout-rev">
          <aside className="dash-card ms-report-list">
            <div className="ms-report-filters no-print">
              <input className="ms-input w-full" id="ms-report-q" placeholder="全文検索（件名・本文・出典）" value={q} onChange={(e) => setQ(e.target.value)} />
              <div className="flex gap-2 flex-wrap mt-2">
                {mode === 'archive' && <select className="ms-input" value={kindF} onChange={(e) => setKindF(e.target.value)}><option value="all">全種別</option>{REPORT_KINDS.map(k => <option key={k.id} value={k.id}>{k.label}</option>)}</select>}
                {mode === 'periodic' && <select className="ms-input" value={kindF} onChange={(e) => setKindF(e.target.value)}><option value="weekly+monthly">週次＋月次</option><option value="weekly">週次</option><option value="monthly">月次</option></select>}
                <select className="ms-input" value={monthF} onChange={(e) => setMonthF(e.target.value)}><option value="all">全期間</option>{months.map(m => <option key={m} value={m}>{m}</option>)}</select>
                <button className={`news-filter ${sort === 'desc' ? 'is-on' : ''}`} onClick={() => setSort(s => s === 'desc' ? 'asc' : 'desc')} title="日付順"><i className={`fas ${sort === 'desc' ? 'fa-arrow-down-wide-short' : 'fa-arrow-up-short-wide'}`}></i>{sort === 'desc' ? '新しい順' : '古い順'}</button>
                <button className={`news-filter ${bmOnly ? 'is-on' : ''}`} onClick={() => setBmOnly(b => !b)}><i className="fas fa-bookmark"></i>あとで読む<span>{store.bookmarks.length}</span></button>
              </div>
              <label className="ms-chip mt-2 ms-chip-sm"><input type="checkbox" checked={showDemo} onChange={(e) => setShowDemo(e.target.checked)} />表示例（デモ）を表示</label>
            </div>
            {!list.length ? <Empty icon="fa-file-circle-question" title={all.length ? 'この条件のレポートはありません' : 'レポート未登録'} sub={all.length ? '' : '右上の「登録」ボタンから手動で登録できます。架空の記事を自動補完することはありません。'} />
              : <ul className="ms-report-items">{list.map(r => { const k = REPORT_KINDS.find(x => x.id === r.kind); return (
                <li key={r.id} className={`ms-report-item ${sel === r.id ? 'is-on' : ''}`} onClick={() => setSel(r.id)}>
                  <div className="ms-report-item-head"><Tag kind={r.kind}>{k?.label}</Tag><span className="ms-report-date">{r.date}</span>{r.isDemo && <Tag kind="demo">デモ</Tag>}{isBm(r.id) && <i className="fas fa-bookmark ms-bm"></i>}</div>
                  <div className="ms-report-item-title">{r.title}</div>
                  {r.fields?.length > 0 && <div className="ms-report-item-fields">{r.fields.map(f => <span key={f}>{FIELDS.find(x => x.id === f)?.label}</span>)}</div>}
                </li>
              ); })}</ul>}
          </aside>
          <main className="dash-card ms-report-view">
            {!current ? <Empty icon="fa-hand-pointer" title="レポートを選択してください" sub={all.length ? '' : 'まだレポートがありません。'} /> : (
              <>
                {current.isDemo && <div className="ms-demo-banner"><i className="fas fa-flask"></i>これは表示例（デモデータ）です。実際のニュースではありません。</div>}
                <div className="ms-report-head">
                  <div><div className="ms-report-title">{current.title}</div><div className="ms-report-meta"><Tag kind={current.kind}>{REPORT_KINDS.find(k => k.id === current.kind)?.label}</Tag><span>対象日 {current.date}</span><span>出典：{current.source || '—'}</span>{current.fields?.map(f => <span key={f} className="ms-field">{FIELDS.find(x => x.id === f)?.label}</span>)}</div></div>
                  <div className="ms-report-tools no-print">
                    <button className={`btn-mini ${isBm(current.id) ? 'is-on' : ''}`} title={isBm(current.id) ? 'あとで読むを解除' : 'あとで読む'} onClick={() => toggleBm(current)}><i className={`${isBm(current.id) ? 'fas' : 'far'} fa-bookmark`}></i></button>
                    <button className="btn-mini" title="Markdownをコピー" onClick={() => copyText(reportToMarkdown(current), toast, 'Markdownをコピーしました')}><i className="fas fa-copy"></i></button>
                    <button className="btn-mini" title="Markdownを保存" onClick={() => downloadText(`IPB_${current.kind}_${current.date}.md`, reportToMarkdown(current))}><i className="fas fa-download"></i></button>
                    <button className="btn-mini" title="スライド用箇条書きをコピー" onClick={() => copyText(reportToBullets(current), toast, 'スライド用箇条書きをコピーしました')}><i className="fas fa-list"></i></button>
                    <button className="btn-mini" title="印刷（PDF保存）" onClick={() => printReport(current)}><i className="fas fa-print"></i></button>
                    <button className="btn-mini" title="配信プレビュー（実送信なし）" onClick={() => setPreview({ report: current })}><i className="fas fa-envelope-open-text"></i></button>
                    {!current.isDemo && <><button className="btn-mini" title="編集" onClick={() => setEditing({ ...current })}><i className="fas fa-pen"></i></button><button className="btn-mini" title="削除" onClick={() => setConfirmDel(current)}><i className="fas fa-trash"></i></button></>}
                  </div>
                </div>
                <div className="ms-md">{renderMd(current.body)}</div>
              </>
            )}
          </main>
        </div>
      </div>
    );
  }
  function mdToHtml(md) {
    const esc = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    const inl = (s) => esc(s).replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>');
    const lines = String(md || '').split('\n'); let html = ''; let inList = false; let table = null;
    const flush = () => { if (inList) { html += '</ul>'; inList = false; } if (table) { const [h, ...b] = table; html += `<table><thead><tr>${h.map(c => `<th>${inl(c)}</th>`).join('')}</tr></thead><tbody>${b.map(r => `<tr>${r.map(c => `<td>${inl(c)}</td>`).join('')}</tr>`).join('')}</tbody></table>`; table = null; } };
    lines.forEach(ln => { const t = ln.trim(); if (!t) { flush(); return; }
      if (/^\|.*\|$/.test(t)) { if (/^\|[\s:\-|]+\|$/.test(t)) return; if (inList) flush(); table = table || []; table.push(t.slice(1, -1).split('|').map(c => c.trim())); return; } else if (table) flush();
      let m; if ((m = t.match(/^##\s+(.+)/))) { flush(); html += `<h2>${inl(m[1])}</h2>`; return; } if ((m = t.match(/^###\s+(.+)/))) { flush(); html += `<h3>${inl(m[1])}</h3>`; return; }
      if ((m = t.match(/^[-*]\s+(.*)/))) { if (!inList) { html += '<ul>'; inList = true; } html += `<li>${inl(m[1].replace(/^\[( |x)\]\s*/, (_, x) => x === 'x' ? '☑ ' : '☐ '))}</li>`; return; }
      flush(); html += `<p>${inl(t)}</p>`; });
    flush(); return html;
  }
  function DeliveryPreview({ report, settings, onChangeSettings, onClose, toast }) {
    const [ch, setCh] = useState(settings.previewChannel || 'mail');
    const [to, setTo] = useState(settings.previewTo || '');
    const bullets = reportToBullets(report);
    return (
      <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
        <div className="modal-card ms-preview"><div className="modal-head"><div><div className="modal-title"><i className="fas fa-envelope-open-text mr-2"></i>配信プレビュー</div><div className="modal-sub">見え方の確認のみ。<b>実際の送信は行いません</b>（メール・LINE連携は別途外部サービス接続が必要）。</div></div><button className="modal-close" onClick={onClose}><i className="fas fa-xmark"></i></button></div>
          <div className="modal-body">
            <div className="flex gap-2 flex-wrap items-center mb-3">
              <button className={`news-filter ${ch === 'mail' ? 'is-on' : ''}`} onClick={() => { setCh('mail'); onChangeSettings({ previewChannel: 'mail' }); }}><i className="fas fa-envelope"></i>HTMLメール</button>
              <button className={`news-filter ${ch === 'line' ? 'is-on' : ''}`} onClick={() => { setCh('line'); onChangeSettings({ previewChannel: 'line' }); }}><i className="fab fa-line"></i>LINE</button>
              <input className="ms-input" placeholder="宛先の表示用メモ（送信しません）" value={to} onChange={(e) => { setTo(e.target.value); onChangeSettings({ previewTo: e.target.value }); }} />
            </div>
            {ch === 'mail' ? (
              <div className="ms-mail"><div className="ms-mail-head"><div><b>件名：</b>[IPB] {report.title}</div><div><b>宛先：</b>{to || '（未設定）'}</div></div><div className="ms-mail-body"><div className="ms-mail-brand">Iwaki Pharma Board — {REPORT_KINDS.find(k => k.id === report.kind)?.label}／{report.date}</div><div dangerouslySetInnerHTML={{ __html: mdToHtml(report.body) }}></div><div className="ms-mail-foot">このメールはプレビューです。実送信は行われません。</div></div></div>
            ) : (
              <div className="ms-line"><div className="ms-line-bubble"><b>【IPB {REPORT_KINDS.find(k => k.id === report.kind)?.label} {report.date}】</b><br />{report.title}<br /><br />{bullets.split('\n').slice(0, 12).map((l, i) => <React.Fragment key={i}>{l}<br /></React.Fragment>)}{bullets.split('\n').length > 12 && '…（続きはIPBで）'}</div><div className="ms-line-note">LINEは長文が読みにくいため、先頭12行までを表示する想定です。</div></div>
            )}
            <div className="flex gap-2 justify-end mt-3"><button className="btn btn-secondary" onClick={() => copyText(ch === 'mail' ? reportToMarkdown(report) : bullets, toast)}><i className="fas fa-copy"></i>本文をコピー</button><button className="btn btn-secondary" onClick={onClose}>閉じる</button></div>
          </div></div>
      </div>
    );
  }

  /* =====================================================================
     学習ノート（セミナーの学び）
     - 保存先: localStorage 'ipb_learn_v1'
     - 入力(要約) → AIで構造化(/api/learn/distill) → 編集 → 保存
     - 他のAI（総評・会議資料・比較・ニュース展望）と経営相談室に自動注入
     - window.IPB_LEARN = { getAll, selectRelevant, markUsed, subscribe }
     ===================================================================== */
  const LEARN_KEY = 'ipb_learn_v1';
  const LEARN_THEMES = [
    { id: 'economy', label: '日本経済と経営判断' }, { id: 'wages', label: '賃上げと収益の両立' }, { id: 'invest', label: '設備投資・資金繰り' },
    { id: 'people', label: '人材定着・店舗運営' }, { id: 'plan', label: '事業計画・幹部会' },
  ];
  const THEME_HINTS = {
    economy: ['金利', '物価', '為替', '円安', '日銀', 'インフレ', '景気', '経済', 'GDP', '北陸', '石川'],
    wages:   ['賃上げ', '賃金', '人件費', '給与', '評価', 'ベア', '昇給', '最低賃金', '賞与', '処遇'],
    invest:  ['投資', '設備', 'DX', '資金繰り', '補助金', '借入', '融資', '自動', '機器', 'ロボ', '監査'],
    people:  ['採用', '定着', '離職', 'シフト', '教育', '研修', '人材', 'モチベ', '働き方', '残業', '有休'],
    plan:    ['事業計画', 'KPI', '幹部', '会議', '承継', '戦略', '中期', '目標', '計画', 'M&A', '在宅', '加算', '調剤報酬', '薬価'],
  };
  function readLearn() { try { const o = JSON.parse(localStorage.getItem(LEARN_KEY) || 'null'); if (o && Array.isArray(o.items)) return { version: 1, ...o }; } catch {} return { version: 1, items: [] }; }
  function writeLearn(s) { try { localStorage.setItem(LEARN_KEY, JSON.stringify(s)); } catch {} window.dispatchEvent(new CustomEvent('ipb-learn-change')); }
  function useLearnVersion() { const [v, setV] = useState(0); useEffect(() => { const h = () => setV(x => x + 1); window.addEventListener('ipb-learn-change', h); return () => window.removeEventListener('ipb-learn-change', h); }, []); return v; }
  function useLearnStore() {
    const [store, setStore] = useState(readLearn);
    const update = useCallback((fn) => setStore(prev => { const next = fn(structuredClone(prev)); writeLearn(next); return next; }), []);
    return [store, update];
  }
  const tokenize = (s) => Array.from(new Set(String(s || '').toLowerCase().replace(/[\s、。，．,.;:：;（）()\[\]「」『』・／/]+/g, ' ').split(' ').map(t => t.trim()).filter(t => t.length >= 2)));
  /* 関連度スコア: テーマ一致(3) + タグ/タイトル語の一致(1.5/語) + 重み + 新しさ(0〜1) */
  function selectRelevant({ themeId, tags, text, limit = 6 } = {}) {
    const items = readLearn().items.filter(k => !k.archived);
    if (!items.length) return [];
    const q = tokenize([text, ...(tags || [])].filter(Boolean).join(' '));
    const hints = themeId ? (THEME_HINTS[themeId] || []) : [];
    const now = Date.now();
    const scored = items.map(k => {
      let s = 0;
      if (themeId && (k.themes || []).includes(themeId)) s += 3;
      const bag = [k.title, ...(k.tags || []), ...(k.principles || []), ...(k.applications || [])].join(' ').toLowerCase();
      q.forEach(t => { if (bag.includes(t)) s += 1.5; });
      hints.forEach(h => { if (bag.includes(h.toLowerCase())) s += 0.6; });
      s += (Number(k.weight) || 0) * 0.8;
      const age = (now - new Date(k.updatedAt || k.createdAt || now).getTime()) / 86400000;
      s += Math.max(0, 1 - age / 365);
      return { k, s };
    }).filter(x => x.s > 0.5 || !themeId && !q.length);
    scored.sort((a, b) => b.s - a.s);
    return scored.slice(0, limit).map(x => x.k);
  }
  function markUsed(ids, where) {
    if (!ids?.length) return;
    const s = readLearn(); const t = nowISO(); let changed = false;
    s.items.forEach(k => { if (ids.includes(k.id)) { k.useCount = (k.useCount || 0) + 1; k.lastUsedAt = t; k.usedIn = Array.from(new Set([...(k.usedIn || []), where].filter(Boolean))).slice(-6); changed = true; } });
    if (changed) writeLearn(s);
  }
  window.IPB_LEARN = { getAll: () => readLearn().items, selectRelevant, markUsed, THEMES: LEARN_THEMES };

  /* ---------- セミナー要約ファイル → テキスト抽出（ブラウザ内。サーバへは送らない） ----------
     対応: .txt .md .markdown .csv .tsv .json（テキスト）／ .pdf（pdf.js を遅延読込）／ .docx（JSZip で word/document.xml を解析）
     非対応: .doc（旧Word）.pptx .xlsx 画像 → エラーメッセージで案内 */
  const LEARN_ACCEPT = '.txt,.md,.markdown,.csv,.tsv,.json,.pdf,.docx,text/plain,text/markdown,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
  const LEARN_MAX_BYTES = 15 * 1024 * 1024;
  const loadScriptOnce = (src, test) => new Promise((resolve, reject) => {
    if (test()) return resolve();
    const ex = document.querySelector(`script[data-src="${src}"]`);
    if (ex) { ex.addEventListener('load', () => resolve()); ex.addEventListener('error', () => reject(new Error('ライブラリの読み込みに失敗'))); return; }
    const sc = document.createElement('script'); sc.src = src; sc.async = true; sc.dataset.src = src;
    sc.onload = () => resolve(); sc.onerror = () => reject(new Error('ライブラリの読み込みに失敗（ネットワークをご確認ください）'));
    document.head.appendChild(sc);
  });
  function readAsTextSmart(f) {
    return new Promise((resolve, reject) => {
      const r1 = new FileReader(); r1.onerror = () => reject(new Error(`「${f.name}」の読み込みに失敗しました`));
      r1.onload = (ev) => { const text = String(ev.target.result || ''); const bad = (text.match(/\uFFFD/g) || []).length;
        if (bad > 0 && bad / Math.max(1, text.length) > 0.005) { const r2 = new FileReader(); r2.onerror = () => resolve(text); r2.onload = (e2) => resolve(String(e2.target.result || '')); try { r2.readAsText(f, 'Shift_JIS'); } catch { resolve(text); } }
        else resolve(text); };
      r1.readAsText(f, 'UTF-8');
    });
  }
  async function extractPdfText(f, onProgress) {
    await loadScriptOnce('https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js', () => !!window.pdfjsLib);
    const pdfjs = window.pdfjsLib; pdfjs.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
    const buf = await f.arrayBuffer();
    const doc = await pdfjs.getDocument({ data: buf, cMapUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/cmaps/', cMapPacked: true, standardFontDataUrl: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/standard_fonts/', useSystemFonts: true }).promise;
    const n = Math.min(doc.numPages, 60); const parts = [];
    for (let i = 1; i <= n; i++) {
      onProgress?.(`PDF ${i}/${n} ページを抽出中…`);
      const page = await doc.getPage(i); const tc = await page.getTextContent();
      let line = '', lastY = null; const lines = [];
      for (const it of tc.items) { const y = Math.round(it.transform?.[5] ?? 0); if (lastY !== null && Math.abs(y - lastY) > 2) { lines.push(line); line = ''; } line += it.str; if (it.hasEOL) { lines.push(line); line = ''; } lastY = y; }
      if (line) lines.push(line);
      parts.push(lines.join('\n').replace(/[ \t]+\n/g, '\n'));
    }
    const text = parts.join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
    if (!text) throw new Error('PDFから文字を取り出せませんでした（画像スキャンのPDFは非対応です。テキストをコピーして貼り付けてください）');
    return { text, note: doc.numPages > n ? `（${doc.numPages}ページ中 先頭${n}ページを抽出）` : `（${doc.numPages}ページ）` };
  }
  async function extractDocxText(f) {
    await loadScriptOnce('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js', () => typeof window.JSZip !== 'undefined');
    const zip = await window.JSZip.loadAsync(await f.arrayBuffer());
    const entry = zip.file('word/document.xml'); if (!entry) throw new Error('Word文書の本文（document.xml）が見つかりません');
    const xml = await entry.async('string');
    const dom = new DOMParser().parseFromString(xml, 'application/xml');
    const paras = Array.from(dom.getElementsByTagNameNS('*', 'p'));
    const lines = paras.map(p => { const isList = p.getElementsByTagNameNS('*', 'numPr').length > 0; const t = Array.from(p.getElementsByTagNameNS('*', 't')).map(x => x.textContent).join(''); return (isList && t ? '・' : '') + t; });
    const text = lines.join('\n').replace(/\n{3,}/g, '\n\n').trim();
    if (!text) throw new Error('Word文書から文字を取り出せませんでした');
    return { text, note: `（${paras.length}段落）` };
  }
  async function extractLearnFile(f, onProgress) {
    if (f.size > LEARN_MAX_BYTES) throw new Error(`「${f.name}」は15MBを超えています`);
    const ext = (f.name.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase();
    if (ext === 'pdf' || f.type === 'application/pdf') return extractPdfText(f, onProgress);
    if (ext === 'docx') return extractDocxText(f);
    if (['doc', 'ppt', 'pptx', 'xls', 'xlsx', 'png', 'jpg', 'jpeg', 'heic', 'gif'].includes(ext)) throw new Error(`「.${ext}」は非対応です。PDF／Word(.docx)／テキスト(.txt .md)で保存し直すか、本文をコピーして貼り付けてください`);
    const text = (await readAsTextSmart(f)).trim();
    if (!text) throw new Error(`「${f.name}」は空のファイルです`);
    return { text, note: '' };
  }

  const emptyDraft = () => ({ title: '', source: '', date: todayStr(), themes: [], tags: [], summary: '', principles: [], numbers: [], checklist: [], applications: [], quotes: [] });
  const LIST_FIELDS = [
    { id: 'principles',   label: '原則・教訓',        icon: 'fa-lightbulb',   ph: '例：技術料を上げるには加算より「枚数×単価」の分解から見る' },
    { id: 'numbers',      label: '覚えておく数字・基準', icon: 'fa-hashtag',    ph: '例：人件費率は技術料の55%以内が目安' },
    { id: 'checklist',    label: '自社で確認すること',   icon: 'fa-list-check', ph: '例：店舗別の後発品率を月次で確認' },
    { id: 'applications', label: '5店舗への適用仮説',   icon: 'fa-store',      ph: '例：南ヶ丘病院店は門前のため…の可能性（仮説）' },
    { id: 'quotes',       label: '印象に残った言葉',    icon: 'fa-quote-left', ph: '例：「経営は数字で語れ、現場は言葉で動かせ」' },
  ];
  const learnToMarkdown = (k) => {
    const L = [`# ${k.title}`, '', `- 出典: ${k.source || '—'}／日付: ${k.date || '—'}`, `- テーマ: ${(k.themes || []).map(t => LEARN_THEMES.find(x => x.id === t)?.label || t).join('、') || '—'}`, `- タグ: ${(k.tags || []).join(', ') || '—'}`, ''];
    if (k.summary) L.push('## 要点', k.summary, '');
    LIST_FIELDS.forEach(f => { if (k[f.id]?.length) { L.push(`## ${f.label}`); k[f.id].forEach(x => L.push(`- ${x}`)); L.push(''); } });
    if (k.rawText) L.push('## 元の要約メモ', '', k.rawText, '');
    return L.join('\n');
  };

  /* 配列フィールドの編集 UI（1行=1項目） */
  function ListEditor({ field, values, onChange }) {
    const [adding, setAdding] = useState('');
    const commit = () => { const t = adding.trim(); if (!t) return; onChange([...(values || []), t]); setAdding(''); };
    return (
      <div className="ms-learn-field" data-field={field.id}>
        <div className="ms-learn-field-head"><i className={`fas ${field.icon}`}></i>{field.label}<span className="ms-learn-count">{(values || []).length}</span></div>
        <ul className="ms-learn-list">
          {(values || []).map((v, i) => (
            <li key={i}>
              <input className="ms-input flex-1" value={v} onChange={(e) => onChange(values.map((x, j) => j === i ? e.target.value : x))} />
              <button type="button" className="ms-icon-btn" title="削除" onClick={() => onChange(values.filter((_, j) => j !== i))}><i className="fas fa-xmark"></i></button>
            </li>
          ))}
          <li className="is-add">
            <input className="ms-input flex-1" placeholder={field.ph} value={adding} onChange={(e) => setAdding(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); commit(); } }} />
            <button type="button" className="ms-icon-btn" title="追加" onClick={commit}><i className="fas fa-plus"></i></button>
          </li>
        </ul>
      </div>
    );
  }

  function LearnPage({ toast, STORES }) {
    const [store, update] = useLearnStore();
    const [mode, setMode] = useState('list'); // list | new | edit
    const [editId, setEditId] = useState(null);
    const [raw, setRaw] = useState('');
    const [draft, setDraft] = useState(emptyDraft);
    const [distilling, setDistilling] = useState(false);
    const [distillErr, setDistillErr] = useState('');
    const [distilled, setDistilled] = useState(false);
    const [aiOk, setAiOk] = useState(null);
    const [q, setQ] = useState('');
    const [themeF, setThemeF] = useState('all');
    const [tagF, setTagF] = useState('');
    const [sel, setSel] = useState(null);
    const [confirmDel, setConfirmDel] = useState(null);
    const [tagInput, setTagInput] = useState('');
    const [fileBusy, setFileBusy] = useState('');
    const [fileErr, setFileErr] = useState('');
    const [fileInfo, setFileInfo] = useState(null); // { names: [], note }
    const [dragOn, setDragOn] = useState(false);
    const fileInputRef = useRef(null);
    const ingestFiles = async (list) => {
      const files = Array.from(list || []); if (!files.length) return;
      setFileErr(''); setFileBusy(`${files[0].name} を読み込み中…`);
      const got = []; const errs = [];
      for (const f of files.slice(0, 5)) {
        try { const r = await extractLearnFile(f, (m) => setFileBusy(`${f.name}：${m}`)); got.push({ name: f.name, ...r }); }
        catch (e) { errs.push(String(e.message || e)); }
      }
      setFileBusy('');
      if (got.length) {
        const combined = got.map(g => (got.length > 1 ? `【${g.name}】\n` : '') + g.text).join('\n\n');
        setRaw(prev => (prev.trim() ? prev.trimEnd() + '\n\n' + combined : combined));
        setDraft(d => ({ ...d, title: d.title || got[0].name.replace(/\.[a-z0-9]+$/i, '').slice(0, 80) }));
        setFileInfo({ names: got.map(g => g.name), note: got.map(g => g.note).filter(Boolean).join(' '), chars: combined.length });
        toast?.(`${got.length}件のファイルから ${combined.length.toLocaleString()} 文字を取り込みました${combined.length > 6000 ? '（AI構造化には先頭6,000文字を送信）' : ''}`);
      }
      if (errs.length) setFileErr(errs.join('／'));
    };
    useEffect(() => { fetch('/api/insight/health').then(r => r.json()).then(j => setAiOk(!!j.configured)).catch(() => setAiOk(false)); }, []);

    const items = store.items;
    const allTags = useMemo(() => { const m = new Map(); items.forEach(k => (k.tags || []).forEach(t => m.set(t, (m.get(t) || 0) + 1))); return Array.from(m.entries()).sort((a, b) => b[1] - a[1]).slice(0, 24); }, [items]);
    const filtered = useMemo(() => {
      const qq = q.trim().toLowerCase();
      return items.filter(k => (themeF === 'all' || (k.themes || []).includes(themeF)) && (!tagF || (k.tags || []).includes(tagF)) && (!qq || [k.title, k.source, k.summary, ...(k.tags || []), ...(k.principles || []), ...(k.numbers || []), k.rawText].join(' ').toLowerCase().includes(qq)))
        .sort((a, b) => (b.weight || 0) - (a.weight || 0) || String(b.updatedAt).localeCompare(String(a.updatedAt)));
    }, [items, q, themeF, tagF]);

    const startNew = () => { setMode('new'); setEditId(null); setRaw(''); setDraft(emptyDraft()); setDistilled(false); setDistillErr(''); };
    const startEdit = (k) => { setMode('edit'); setEditId(k.id); setRaw(k.rawText || ''); setDraft({ ...emptyDraft(), ...k }); setDistilled(true); setDistillErr(''); };
    const cancel = () => { setMode('list'); setEditId(null); };

    const distill = async () => {
      const text = raw.trim();
      if (text.length < 20) { toast?.('要約メモを20文字以上入力してください'); return; }
      setDistilling(true); setDistillErr('');
      try {
        const res = await fetch('/api/learn/distill', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: text.slice(0, 6000), title: draft.title || undefined, source: draft.source || undefined }) });
        const j = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`);
        setDraft(d => ({ ...d, title: d.title || j.title || '', summary: j.summary || '', themes: j.themes?.length ? j.themes : d.themes, tags: Array.from(new Set([...(d.tags || []), ...(j.tags || [])])).slice(0, 10), principles: j.principles || [], numbers: j.numbers || [], checklist: j.checklist || [], applications: j.applications || [], quotes: j.quotes || [] }));
        setDistilled(true); toast?.('AIが学びを構造化しました。内容を確認・修正して保存してください');
      } catch (e) { setDistillErr(String(e.message || e)); setDistilled(true); }
      finally { setDistilling(false); }
    };
    const manual = () => { setDistilled(true); setDraft(d => ({ ...d, title: d.title || raw.split('\n').find(l => l.trim())?.trim().slice(0, 60) || '' })); };
    const save = () => {
      const title = draft.title.trim(); if (!title) { toast?.('タイトルを入力してください'); return; }
      const clean = (a) => (a || []).map(x => String(x).trim()).filter(Boolean);
      const body = { title, source: draft.source.trim(), date: draft.date, themes: draft.themes, tags: clean(draft.tags), summary: (draft.summary || '').trim(), principles: clean(draft.principles), numbers: clean(draft.numbers), checklist: clean(draft.checklist), applications: clean(draft.applications), quotes: clean(draft.quotes), rawText: raw.trim() };
      if (!body.principles.length && !body.numbers.length && !body.checklist.length && !body.applications.length && !body.summary) { toast?.('原則・数字・確認事項・適用仮説のいずれかを1つ以上入力してください'); return; }
      const id = editId || uid();
      update(s => {
        if (editId) { s.items = s.items.map(k => k.id === id ? { ...k, ...body, updatedAt: nowISO() } : k); }
        else { s.items.unshift({ id, ...body, weight: 1, useCount: 0, usedIn: [], createdAt: nowISO(), updatedAt: nowISO() }); }
        return s;
      });
      toast?.(editId ? '学びを更新しました' : '学びを保存しました。以後のAI総評・会議資料・ニュース展望・経営相談室で自動的に参照されます');
      setMode('list'); setSel(id); setEditId(null);
    };
    const bump = (k, d) => update(s => { const x = s.items.find(i => i.id === k.id); if (x) { x.weight = Math.max(0, Math.min(5, (x.weight || 1) + d)); x.updatedAt = nowISO(); } return s; });
    const remove = (k) => update(s => { s.items = s.items.filter(i => i.id !== k.id); return s; });
    const toggleTheme = (t) => setDraft(d => ({ ...d, themes: d.themes.includes(t) ? d.themes.filter(x => x !== t) : [...d.themes, t] }));
    const addTag = () => { const t = tagInput.trim().replace(/^#/, ''); if (!t) return; setDraft(d => ({ ...d, tags: Array.from(new Set([...(d.tags || []), t])).slice(0, 12) })); setTagInput(''); };
    const exportAll = () => downloadText(`learning_notes_${todayStr()}.md`, items.map(learnToMarkdown).join('\n\n---\n\n'));
    const selected = items.find(k => k.id === sel) || null;
    const usedTotal = items.reduce((a, k) => a + (k.useCount || 0), 0);

    const Head = (
      <div className="yoy-page-head ms-head">
        <div><div className="yoy-page-title"><i className="fas fa-graduation-cap mr-2"></i>学習ノート（セミナーの学び）</div>
          <div className="yoy-page-sub">聞いたセミナーの要約を貼ると、AIが「原則・数字・確認事項・5店舗への適用仮説」に構造化して保存。以後の<b>AI総評・会議資料・比較・ニュース展望・経営相談室</b>が、この学びを自動で参照します（引用は [L1] 形式）。</div></div>
        <div className="flex gap-2 flex-wrap no-print">
          {mode === 'list' ? (<>
            <button className="btn btn-secondary" onClick={exportAll} disabled={!items.length}><i className="fas fa-download"></i>MD一括出力</button>
            <button className="btn btn-primary" id="ms-learn-new" onClick={startNew}><i className="fas fa-plus"></i>学びを追加</button>
          </>) : (<>
            <button className="btn btn-secondary" onClick={cancel}><i className="fas fa-arrow-left"></i>一覧へ戻る</button>
            <button className="btn btn-primary" id="ms-learn-save" onClick={save} disabled={!distilled}><i className="fas fa-floppy-disk"></i>{editId ? '更新を保存' : '学びを保存'}</button>
          </>)}
        </div>
      </div>
    );

    if (mode !== 'list') {
      return (
        <div className="ms-page" id="ms-learn">
          {Head}
          <div className="ms-learn-editor">
            <aside className="dash-card ms-form">
              <div className="dash-card-title"><span className="badge">1</span>セミナーの要約を入力</div>
              <label className="ms-label">タイトル（セミナー名・テーマ）</label>
              <input className="ms-input w-full" id="ms-learn-title" value={draft.title} onChange={(e) => setDraft(d => ({ ...d, title: e.target.value }))} placeholder="例：2026年度 調剤報酬改定と薬局経営セミナー" />
              <div className="ms-learn-row">
                <div><label className="ms-label">出典（講師・主催）</label><input className="ms-input w-full" value={draft.source} onChange={(e) => setDraft(d => ({ ...d, source: e.target.value }))} placeholder="例：○○コンサル 講師△△氏" /></div>
                <div><label className="ms-label">受講日</label><input type="date" className="ms-input w-full" value={draft.date} onChange={(e) => setDraft(d => ({ ...d, date: e.target.value }))} /></div>
              </div>
              <label className="ms-label">要約メモ（貼り付け、またはファイルをドロップ）</label>
              <div className={`ms-learn-drop ${dragOn ? 'is-over' : ''} ${fileBusy ? 'is-busy' : ''}`} id="ms-learn-drop" role="button" tabIndex={0}
                onClick={() => !fileBusy && fileInputRef.current?.click()} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInputRef.current?.click(); } }}
                onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); setDragOn(true); }} onDragLeave={(e) => { e.preventDefault(); setDragOn(false); }}
                onDrop={(e) => { e.preventDefault(); e.stopPropagation(); setDragOn(false); ingestFiles(e.dataTransfer?.files); }}>
                <input ref={fileInputRef} type="file" accept={LEARN_ACCEPT} multiple style={{ display: 'none' }} id="ms-learn-file" onChange={(e) => { ingestFiles(e.target.files); e.target.value = ''; }} />
                {fileBusy ? <><i className="fas fa-spinner fa-spin"></i><span>{fileBusy}</span></>
                  : <><i className="fas fa-file-arrow-up"></i><span><b>セミナー要約ファイルをここにドロップ</b>（またはクリックして選択）<br /><small>PDF／Word(.docx)／テキスト(.txt .md)。ファイルはブラウザ内で文字起こしし、サーバには本文テキストのみ送信します</small></span></>}
              </div>
              {fileErr && <div className="ms-caveat is-error ms-learn-fileerr"><i className="fas fa-triangle-exclamation"></i>{fileErr}</div>}
              {fileInfo && <div className="ms-learn-fileinfo"><i className="fas fa-paperclip"></i>{fileInfo.names.join('、')} {fileInfo.note} → {fileInfo.chars.toLocaleString()} 文字を下の要約メモに追加しました<button type="button" className="ms-link" onClick={() => setFileInfo(null)}>閉じる</button></div>}
              <textarea className="ms-input ms-learn-raw" id="ms-learn-raw" rows={14} value={raw} onChange={(e) => setRaw(e.target.value)} placeholder={'例）\n・2026年改定は対物から対人へさらにシフト。地域支援体制加算の要件が厳格化\n・技術料は「枚数×単価」で分解して見る。単価は加算算定率で決まる\n・人件費率は技術料の55%が目安。超えたら配置か単価の見直し\n・門前店は処方元依存リスク。面対応と在宅の比率を3年で20%に…'} />
              <div className="ms-learn-raw-meta">{raw.length.toLocaleString()} 文字{raw.length > 6000 && <span className="text-red-500">（6,000文字まで送信されます）</span>}</div>
              <div className="flex gap-2 flex-wrap mt-2">
                <button className="btn btn-primary" id="ms-learn-distill" onClick={distill} disabled={distilling || raw.trim().length < 20 || aiOk === false}>
                  {distilling ? <><i className="fas fa-spinner fa-spin"></i>AIが構造化中…</> : <><i className="fas fa-wand-magic-sparkles"></i>AIで構造化する</>}
                </button>
                <button className="btn btn-secondary" onClick={manual} disabled={distilling}><i className="fas fa-pen"></i>手入力で整理する</button>
              </div>
              {aiOk === false && <div className="ms-caveat mt-2"><i className="fas fa-circle-info"></i>AIが未設定のため、手入力で整理してください（保存した学びは同じように活用されます）。</div>}
              {distillErr && <div className="ms-caveat mt-2 is-error"><i className="fas fa-triangle-exclamation"></i>AI構造化に失敗：{distillErr}。手入力で続けられます。</div>}
              <div className="ms-note mt-3">AIはメモに無い事実・数字を補いません。構造化の結果は必ず右側で確認し、違っていれば直してから保存してください。</div>
            </aside>
            <section className={`dash-card ms-learn-struct ${distilled ? '' : 'is-waiting'}`} id="ms-learn-struct">
              <div className="dash-card-title"><span className="badge">2</span>構造化された学び（編集できます）</div>
              {!distilled ? (
                <Empty icon="fa-wand-magic-sparkles" title="まだ構造化されていません" sub="左でメモを入力し「AIで構造化する」または「手入力で整理する」を押してください。" />
              ) : (<>
                <label className="ms-label">要点（120字以内）</label>
                <textarea className="ms-input w-full" rows={2} value={draft.summary} onChange={(e) => setDraft(d => ({ ...d, summary: e.target.value }))} />
                <label className="ms-label">関連テーマ（経営相談室のテーマと連動）</label>
                <div className="ms-chips">{LEARN_THEMES.map(t => <label key={t.id} className={`ms-chip ${draft.themes.includes(t.id) ? 'is-on' : ''}`}><input type="checkbox" checked={draft.themes.includes(t.id)} onChange={() => toggleTheme(t.id)} />{t.label}</label>)}</div>
                <label className="ms-label">タグ（検索・関連付けに使用）</label>
                <div className="ms-learn-tags">
                  {(draft.tags || []).map(t => <span key={t} className="ms-learn-tag">#{t}<button type="button" onClick={() => setDraft(d => ({ ...d, tags: d.tags.filter(x => x !== t) }))}><i className="fas fa-xmark"></i></button></span>)}
                  <input className="ms-input" placeholder="タグを追加して Enter" value={tagInput} onChange={(e) => setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTag(); } }} />
                </div>
                {LIST_FIELDS.map(f => <ListEditor key={f.id} field={f} values={draft[f.id]} onChange={(v) => setDraft(d => ({ ...d, [f.id]: v }))} />)}
                <div className="flex gap-2 justify-end mt-3"><button className="btn btn-secondary" onClick={cancel}>キャンセル</button><button className="btn btn-primary" onClick={save}><i className="fas fa-floppy-disk"></i>{editId ? '更新を保存' : '学びを保存'}</button></div>
              </>)}
            </section>
          </div>
        </div>
      );
    }

    return (
      <div className="ms-page" id="ms-learn">
        {confirmDel && <ConfirmDialog title="学びを削除" body={`「${confirmDel.title}」を削除します。以後のAI分析・相談ガイドでは参照されなくなります。元に戻せません。`} onOk={() => { remove(confirmDel); if (sel === confirmDel.id) setSel(null); setConfirmDel(null); toast?.('削除しました'); }} onCancel={() => setConfirmDel(null)} />}
        {Head}
        <div className="ms-learn-stats">
          <div className="ms-learn-stat"><b>{items.length}</b><span>件の学び</span></div>
          <div className="ms-learn-stat"><b>{usedTotal}</b><span>回 AI・相談で参照</span></div>
          <div className="ms-learn-stat"><b>{allTags.length}</b><span>種類のタグ</span></div>
          <div className="ms-learn-howto"><i className="fas fa-circle-info"></i>学びは「テーマ一致・タグ一致・重み（役に立った）・新しさ」で関連度を計算し、各AI分析に最大6件、経営相談室に最大4件を自動で添えます。</div>
        </div>
        {items.length === 0 ? (
          <div className="dash-card"><Empty icon="fa-graduation-cap" title="まだ学びがありません" sub="「学びを追加」から、聞いたセミナーの要約を貼り付けてください。AIが構造化し、以後の分析に活きてきます。" /></div>
        ) : (
          <div className="ms-learn-layout">
            <section className="dash-card ms-learn-listcard">
              <div className="ms-toolbar">
                <input className="ms-input flex-1" id="ms-learn-q" placeholder="検索（タイトル・タグ・原則・数字…）" value={q} onChange={(e) => setQ(e.target.value)} />
                <select className="ms-input" value={themeF} onChange={(e) => setThemeF(e.target.value)}><option value="all">全テーマ</option>{LEARN_THEMES.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}</select>
              </div>
              {allTags.length > 0 && <div className="ms-learn-tagcloud">{allTags.map(([t, n]) => <button key={t} type="button" className={`ms-learn-tag is-btn ${tagF === t ? 'is-on' : ''}`} onClick={() => setTagF(tagF === t ? '' : t)}>#{t}<small>{n}</small></button>)}</div>}
              <ul className="ms-learn-items">
                {filtered.map(k => (
                  <li key={k.id} className={`ms-learn-item ${sel === k.id ? 'is-sel' : ''}`} onClick={() => setSel(k.id)}>
                    <div className="ms-learn-item-head"><span className="ms-learn-item-title">{k.title}</span><span className="ms-learn-weight" title="重み（役に立った度）">{'★'.repeat(Math.max(1, Math.round(k.weight || 1)))}</span></div>
                    <div className="ms-learn-item-meta">{k.source && <span>{k.source}</span>}{k.date && <span>{k.date}</span>}{(k.themes || []).map(t => <Tag key={t} kind="learn">{LEARN_THEMES.find(x => x.id === t)?.label}</Tag>)}{k.useCount > 0 && <span className="ms-learn-used"><i className="fas fa-robot"></i>{k.useCount}回参照</span>}</div>
                    {k.summary && <div className="ms-learn-item-sum">{k.summary}</div>}
                  </li>
                ))}
                {!filtered.length && <li className="ms-learn-none">条件に合う学びがありません</li>}
              </ul>
            </section>
            <section className="dash-card ms-learn-detail" id="ms-learn-detail">
              {!selected ? <Empty icon="fa-hand-pointer" title="学びを選択" sub="左の一覧から選ぶと内容を表示します。" /> : (<>
                <div className="ms-learn-detail-head">
                  <div><h3>{selected.title}</h3><div className="ms-learn-item-meta">{selected.source && <span>{selected.source}</span>}{selected.date && <span>{selected.date}</span>}<span>登録 {fmtDate(selected.createdAt)}</span></div></div>
                  <div className="flex gap-1 flex-wrap no-print">
                    <button className="btn btn-secondary btn-sm" title="役に立った（重みを上げる）" onClick={() => { bump(selected, +1); toast?.('重みを上げました。関連度が高くなります'); }}><i className="fas fa-thumbs-up"></i>役に立った</button>
                    <button className="btn btn-secondary btn-sm" title="重みを下げる" onClick={() => bump(selected, -1)}><i className="fas fa-thumbs-down"></i></button>
                    <button className="btn btn-secondary btn-sm" onClick={() => copyText(learnToMarkdown(selected), toast, 'Markdownをコピーしました')}><i className="fas fa-copy"></i>MD</button>
                    <button className="btn btn-secondary btn-sm" id="ms-learn-edit" onClick={() => startEdit(selected)}><i className="fas fa-pen"></i>編集</button>
                    <button className="btn btn-danger btn-sm" onClick={() => setConfirmDel(selected)}><i className="fas fa-trash"></i></button>
                  </div>
                </div>
                <div className="ms-learn-weightbar">重み：{'★'.repeat(Math.max(1, Math.round(selected.weight || 1)))}{'☆'.repeat(Math.max(0, 5 - Math.max(1, Math.round(selected.weight || 1))))}<span>／ 参照 {selected.useCount || 0} 回{selected.usedIn?.length ? `（${selected.usedIn.join('・')}）` : ''}{selected.lastUsedAt ? `／最終 ${fmtDate(selected.lastUsedAt)}` : ''}</span></div>
                {selected.summary && <p className="ms-learn-sum">{selected.summary}</p>}
                <div className="ms-learn-tags">{(selected.themes || []).map(t => <Tag key={t} kind="learn">{LEARN_THEMES.find(x => x.id === t)?.label}</Tag>)}{(selected.tags || []).map(t => <span key={t} className="ms-learn-tag">#{t}</span>)}</div>
                <div className="ms-learn-grid">
                  {LIST_FIELDS.map(f => selected[f.id]?.length > 0 && (
                    <section key={f.id} className="ms-sec"><h4><i className={`fas ${f.icon} mr-1`}></i>{f.label}</h4><ul>{selected[f.id].map((x, i) => <li key={i}>{x}</li>)}</ul></section>
                  ))}
                </div>
                {selected.rawText && <details className="ms-learn-rawbox"><summary>元の要約メモを表示</summary><pre>{selected.rawText}</pre></details>}
                <div className="ms-note mt-2"><i className="fas fa-robot mr-1"></i>この学びは、テーマ・タグが合うときに AI総評／会議資料／ニュース展望／経営相談室へ自動で添付されます（出力中の [L番号] が引用印）。</div>
              </>)}
            </section>
          </div>
        )}
      </div>
    );
  }

  /* ---------- ルート：既存 App から呼ばれるエントリ ---------- */
  const MS_VIEWS = [
    { id: 'ms_consult', label: '経営相談室',       icon: 'fa-comments' },
    { id: 'ms_actions', label: 'アクションプラン',  icon: 'fa-list-check' },
    { id: 'ms_daily',   label: 'デイリーブリーフィング', icon: 'fa-sun' },
    { id: 'ms_periodic',label: '週次・月次レポート', icon: 'fa-calendar-week' },
    { id: 'ms_archive', label: 'アーカイブ',         icon: 'fa-box-archive' },
    { id: 'ms_learn',   label: '学習ノート（セミナーの学び）', icon: 'fa-graduation-cap' },
  ];
  const MS_TITLES = Object.fromEntries(MS_VIEWS.map(v => [v.id, v.label]));

  function MgmtRoot({ view, setView, STORES, showToast, getKpi }) {
    const [store, update] = useMgmtStore();
    const [focusPlan, setFocusPlan] = useState(null);
    const openActions = (pid) => { setFocusPlan(pid); setView('ms_actions'); };
    const openPlan = (pid) => { setFocusPlan(pid); setView('ms_consult'); };
    if (view === 'ms_consult') return <ConsultPage key={focusPlan || 'new'} store={store} update={update} STORES={STORES} toast={showToast} initialPlanId={focusPlan} onOpenActions={openActions} getKpi={getKpi} />;
    if (view === 'ms_actions') return <ActionsPage store={store} update={update} toast={showToast} onOpenPlan={openPlan} focusPlanId={focusPlan} />;
    if (view === 'ms_daily')   return <ReportsPage store={store} update={update} toast={showToast} mode="daily" />;
    if (view === 'ms_periodic')return <ReportsPage store={store} update={update} toast={showToast} mode="periodic" />;
    if (view === 'ms_archive') return <ReportsPage store={store} update={update} toast={showToast} mode="archive" />;
    if (view === 'ms_learn')   return <LearnPage toast={showToast} STORES={STORES} />;
    return null;
  }

  window.IPB_MGMT = { MgmtRoot, MS_VIEWS, MS_TITLES, THEMES, isMsView: (v) => typeof v === 'string' && v.startsWith('ms_') };
})();
