/* ============================================================
   案件詳細（コア画面）+ 商談記録モーダル + ステータス変更メニュー
   ============================================================ */
function StatusMenu({ caseId, current, archived }) {
  const { changeStatus, archiveCase, cases } = useStore();
  const D = window.APP_DATA;
  const [open, setOpen] = React.useState(false);
  const [lostAsk, setLostAsk] = React.useState(false); // 失注へ変える瞬間に理由入力モーダルを挟む
  const [wonAsk, setWonAsk] = React.useState(null); // 成約/受注へ変える瞬間の受注理由モーダル（値=適用するstatus）
  const ref = React.useRef(null);
  React.useEffect(() => { const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []);
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <Button variant="default" iconRight="chevronDown" onClick={() => setOpen(o => !o)}>{t('btn.changeStatus')}</Button>
      {open && (
        <div style={{ position: 'absolute', top: 42, right: 0, zIndex: 70, width: 220, background: '#fff', borderRadius: 11, border: '1px solid #e6e8ec', boxShadow: '0 14px 34px rgba(20,22,40,.16)', padding: 6 }}>
          <div style={{ fontSize: 12, fontWeight: 700, color: '#a8aeb8', padding: '6px 9px 4px', letterSpacing: '.04em' }}>{t('cases.statusFlow')}</div>
          {D.STATUS_ORDER.map(s => (
            <div key={s} className="row-hover" onClick={() => { if (s === 'lost' && current !== 'lost') { setLostAsk(true); } else if ((s === 'won' || s === 'done') && !isCaseWonStatus(current)) { setWonAsk(s); } else { changeStatus(caseId, s); } setOpen(false); }}
              style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 9px', borderRadius: 7, cursor: 'pointer' }}>
              <StatusBadge status={s} size="sm" />
              {s === current && <Icon name="check" size={14} stroke={2.4} style={{ color: '#4a5af0' }} />}
            </div>
          ))}
          {/* アーカイブ（封存）：成件後の継続会議など、アクティブ案件一覧から外す。解除も可能。 */}
          <div style={{ height: 1, background: '#f0f1f4', margin: '6px 4px' }} />
          <div className="row-hover" onClick={() => { archiveCase(caseId, !archived); setOpen(false); }}
            style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 9px', borderRadius: 7, cursor: 'pointer', color: archived ? '#4a5af0' : '#3b414b' }}>
            <Icon name={archived ? 'refresh' : 'inbox'} size={15} stroke={2} style={{ color: archived ? '#4a5af0' : '#8a909b' }} />
            <span style={{ fontSize: 13, fontWeight: 600 }}>{archived ? t('cases.unarchive') : t('cases.archive')}</span>
          </div>
        </div>
      )}
      {lostAsk && (() => { const kc = cases.find(x => x.id === caseId) || (D.caseById && D.caseById(caseId)) || {}; return <LostReasonModal caseId={caseId} initialReason={kc.lostReason} initialNote={kc.lostReasonNote} applyStatus onClose={() => setLostAsk(false)} />; })()}
      {wonAsk && (() => { const kc = cases.find(x => x.id === caseId) || (D.caseById && D.caseById(caseId)) || {}; return <WonReasonModal caseId={caseId} initialReason={kc.wonReason} initialNote={kc.wonReasonNote} applyStatus={wonAsk} onClose={() => setWonAsk(null)} />; })()}
    </div>
  );
}


/* 今回の会議URL・場所（手入力）。会議URLは商談のたびに変わり、直前にメールでやり取りされることが多いため、
   ここで上書きすると Google カレンダーの「場所」欄へ反映し、参加者へ変更通知が飛ぶ。
   人が作った予定（レディクル等）に対しても location だけは更新する（日時・タイトル・説明は触らない。2026-08-12 決定）。
   URL を入れた場合は会議リンク一覧の先頭にも出て、案件の「参加」ボタンがこのURLを使う（ui.jsx caseAllMeetingLinks）。 */
function MeetingPlaceRow({ c, saveCase, canEdit = true }) {
  const [edit, setEdit] = React.useState(false);
  const [v, setV] = React.useState('');
  const cur = String(c.meetingPlace || '').trim();
  const isUrl = /^https?:\/\//i.test(cur);
  const save = () => {
    // Google の location は長すぎると 400 で弾かれ、その失敗が予定の作り直し＝二重登録に落ちるので入口で切る
    let s = v.trim().slice(0, 500);
    // 「zoom.us/j/…」のように scheme 無しで貼られたURLは補う。住所・会議室名はそのまま
    if (s && !/^https?:\/\//i.test(s) && /^(www\.)?[\w-]+(\.[\w-]+)+\/\S/.test(s)) s = 'https://' + s;
    saveCase({ id: c.id, meetingPlace: s });
    setEdit(false);
  };
  const miniInput = { ...inputStyle, padding: '5px 8px', fontSize: 12.5, width: '100%' };
  return (
    <InfoRow icon="mapPin" label={t('cd.meetingPlace.label')}>
      {edit ? (
        <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <input value={v} onChange={(e) => setV(e.target.value)} autoFocus placeholder={t('cd.meetingPlace.placeholder')} maxLength={500}
            onKeyDown={(e) => { if (enterSubmits(e)) save(); if (e.key === 'Escape') setEdit(false); }}
            style={{ ...miniInput, width: 260, maxWidth: '100%' }} />
          <IconButton name="check" size={15} title={t('btn.save')} onClick={save} />
          <IconButton name="x" size={15} title={t('btn.cancel')} onClick={() => setEdit(false)} />
          {!!cur && <button onClick={() => { saveCase({ id: c.id, meetingPlace: '' }); setEdit(false); }}
            style={{ border: 'none', background: 'transparent', color: '#9aa1ab', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', textDecoration: 'underline' }}>{t('cd.meetingPlace.clear')}</button>}
        </div>
      ) : (
        // nowrap：URL が長くても「開く」アイコンを次の行へ落とさず、文字側だけを省略する
        <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'nowrap', minWidth: 0 }}>
          <span onClick={canEdit ? (() => { setV(cur); setEdit(true); }) : undefined} title={canEdit ? t('cd.clickToEdit') : undefined}
            style={{ cursor: canEdit ? 'pointer' : 'default', display: 'inline-flex', alignItems: 'center', gap: 7, minWidth: 0, flex: '0 1 auto', margin: '-2px -7px', padding: '2px 7px', borderRadius: 6, transition: 'background .12s' }}
            onMouseEnter={(e) => { if (canEdit) e.currentTarget.style.background = '#f0f1f4'; }}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
            <span style={{ color: cur ? '#2b2f38' : '#9aa1ab', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{cur || (canEdit ? t('cd.meetingPlace.empty') : t('cd.undecided'))}</span>
            {canEdit && <Icon name="edit" size={12} stroke={2} style={{ color: '#b4bac3', flex: '0 0 auto' }} />}
          </span>
          {isUrl && <a href={cur} target="_blank" rel="noreferrer" style={{ flex: '0 0 auto' }}><IconButton name="link" size={14} title={t('btn.open')} /></a>}
        </div>
      )}
    </InfoRow>
  );
}

/* Googleカレンダー連携行：商談日・担当者の変更はサーバー側で自動同期される（作成/更新/担当外し/削除）。
   ここは同期状態の表示＋「今すぐ送る（再送）」の手動ボタン。オフライン連携(calendar.events)が必要。
   招待は担当者（メイン・サポート＝システム登録済みメンバー）のみ。顧客メールへは送らない（2026-07-09）。 */
function CalInviteRow({ c }) {
  const { showToast } = useStore();
  const mt = c.nextMeeting || c.appointAt;
  const [busy, setBusy] = React.useState(false);
  const [last, setLast] = React.useState({ at: c.calInviteAt, for: c.calInviteFor, n: String(c.calInviteTo || '').split(',').filter(Boolean).length });
  if (!mt && !last.at) return null; // 商談日も同期履歴も無ければ表示しない
  const send = async () => {
    if (busy) return;
    // 確認ダイアログの日時は画面表示（caseMeetingAt）と同じもの＝サーバーが登録する日時と揃える
    const when = caseMeetingAt(c) || mt;
    if (!window.confirm(t('cd.calInvite.confirmMembers', { when: `${fmtDateFull(when)} ${fmtTime(when)}` }))) return;
    setBusy(true);
    try {
      const r = await API.calendarInvite(c.id);
      setLast({ at: r.at || mt, for: r.at || mt, n: (r.invited || []).length || last.n });
      showToast(r && r.mode === 'linked' ? t('cd.calInvite.addedExisting') : r && (r.updated || r.mode === 'updated') ? t('cd.calInvite.updated') : t('cd.calInvite.sent'));
    }
    catch (e) { showToast((e && e.message) || t('cd.calInvite.failed'), 'x'); }
    setBusy(false);
  };
  return (
    <InfoRow icon="calendar" label={t('cd.calInvite.label')}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 9, flexWrap: 'wrap' }}>
        <Button size="sm" variant="default" icon="google" onClick={send} disabled={busy}>{busy ? t('cd.calInvite.sending') : (last.at ? t('cd.calInvite.resend') : t('cd.calInvite.send'))}</Button>
        {!!last.at && (
          <span style={{ fontSize: 12, color: '#16a34a', display: 'inline-flex', alignItems: 'center', gap: 4 }}>
            <Icon name="check2" size={13} stroke={2.2} />
            {last.for ? t('cd.calInvite.syncedInfo', { when: `${fmtDate(last.for)} ${fmtTime(last.for)}`, n: last.n }) : t('cd.calInvite.done')}
          </span>
        )}
        <span style={{ fontSize: 11.5, color: '#b4bac3' }}>{t('cd.calInvite.autoHint')}</span>
      </div>
    </InfoRow>
  );
}

/* 商談記録モーダル + 会議記録タブ は src/case-meetings.jsx へ分離 */

/* 添付タブは src/case-attach.jsx へ分離 */

/* 受注金額（成約金額）行：成約(won)/受注(done)案件のみ表示。手入力すると KPIレポート・顧客集計の
   受注金額に最優先で反映（未入力は見積書合計で近似＝caseWonAmount）。空で保存すると自動（見積合計）に戻る */
function WonAmountRow({ c, saveCase, canEdit }) {
  const [edit, setEdit] = React.useState(false);
  const [v, setV] = React.useState('');
  const manual = c.wonAmount != null && c.wonAmount !== '';
  const amt = caseWonAmount(c);
  const open = () => { setV(manual ? String(c.wonAmount) : String(caseQuoteAmount(c) || '')); setEdit(true); };
  const save = () => { const n = String(v).trim() === '' ? null : Math.max(0, Math.round(Number(v) || 0)); saveCase({ id: c.id, wonAmount: n }); setEdit(false); };
  return (
    <InfoRow icon="check2" label={window.t("extra.case.orderAmount")}>
      {edit ? (
        <span style={{ display: 'inline-flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <input type="number" min="0" step="1000" value={v} onChange={e => setV(e.target.value)} autoFocus
            onKeyDown={e => { if (enterSubmits(e)) save(); if (e.key === 'Escape') setEdit(false); }}
            style={{ width: 140, padding: '5px 8px', borderRadius: 7, border: '1px solid #d9dce2', fontFamily: 'inherit', fontSize: 13 }} placeholder={window.t("extra.case.amountHint")} />
          <button onClick={save} style={{ border: 'none', borderRadius: 7, background: '#4a5af0', color: '#fff', fontFamily: 'inherit', fontSize: 12, fontWeight: 700, padding: '6px 12px', cursor: 'pointer' }}>{window.t("btn.save")}</button>
          <button onClick={() => setEdit(false)} style={{ border: '1px solid #e2e4e9', borderRadius: 7, background: '#fff', color: '#6b7280', fontFamily: 'inherit', fontSize: 12, padding: '5px 10px', cursor: 'pointer' }}>{window.t("btn.cancel")}</button>
        </span>
      ) : (
        <span style={{ display: 'inline-flex', gap: 8, alignItems: 'center' }}>
          <span style={{ fontWeight: 700, color: '#15803d' }}>¥{Math.round(amt).toLocaleString('ja-JP')}</span>
          <span style={{ fontSize: 11, color: '#a8aeb8' }}>{manual ? window.t("extra.common.manualInput") : window.t("extra.case.quoteTotalAuto")}</span>
          {canEdit && <button onClick={open} style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#4a5af0', fontSize: 12, fontWeight: 600, padding: 0 }}>{window.t("btn.edit")}</button>}
        </span>
      )}
    </InfoRow>
  );
}

/* 受注理由行：成約/受注案件のサイドバーに表示。緑チップ＋詳細＋「編集」で WonReasonModal（後追い編集） */
function WonReasonRow({ c, canEdit }) {
  const [edit, setEdit] = React.useState(false);
  const lbl = wonReasonLabel(c.wonReason);
  return (
    <InfoRow icon="check2" label={window.t("extra.case.winReason")}>
      <span style={{ display: 'inline-flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        {lbl
          ? <span style={{ fontWeight: 700, color: '#15803d', background: '#e3f5e9', padding: '2px 10px', borderRadius: 999, fontSize: 12 }}>{lbl}</span>
          : <span style={{ color: '#9aa1ab' }}>{window.t("extra.common.notFilled")}</span>}
        {c.wonReasonNote && <span style={{ fontSize: 12, color: '#6b727c' }}>{c.wonReasonNote}</span>}
        {canEdit && <button onClick={() => setEdit(true)} style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#4a5af0', fontSize: 12, fontWeight: 600, padding: 0 }}>{window.t("btn.edit")}</button>}
      </span>
      {edit && <WonReasonModal caseId={c.id} initialReason={c.wonReason} initialNote={c.wonReasonNote} onClose={() => setEdit(false)} />}
    </InfoRow>
  );
}

/* 失注理由行：失注案件のサイドバーに表示。ラベルチップ＋詳細＋「編集」で LostReasonModal（後追い編集） */
function LostReasonRow({ c, canEdit }) {
  const [edit, setEdit] = React.useState(false);
  const lbl = lostReasonLabel(c.lostReason);
  return (
    <InfoRow icon="x" label={window.t("extra.case.lossReason")}>
      <span style={{ display: 'inline-flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
        {lbl
          ? <span style={{ fontWeight: 700, color: '#b91c1c', background: '#fdecec', padding: '2px 10px', borderRadius: 999, fontSize: 12 }}>{lbl}</span>
          : <span style={{ color: '#9aa1ab' }}>{window.t("extra.common.notFilled")}</span>}
        {c.lostReasonNote && <span style={{ fontSize: 12, color: '#6b727c' }}>{c.lostReasonNote}</span>}
        {canEdit && <button onClick={() => setEdit(true)} style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#4a5af0', fontSize: 12, fontWeight: 600, padding: 0 }}>{window.t("btn.edit")}</button>}
      </span>
      {edit && <LostReasonModal caseId={c.id} initialReason={c.lostReason} initialNote={c.lostReasonNote} onClose={() => setEdit(false)} />}
    </InfoRow>
  );
}

function EditableDateRow({ icon, label, value, withTime, allowClear, onSave, editable = true, children }) {
  // フックは条件分岐より前で必ず宣言（editable が実行時に変わっても hook 順序が崩れないように）
  const [edit, setEdit] = React.useState(false);
  const [d, setD] = React.useState('');
  const [tm, setTm] = React.useState('10:00'); // 時刻state（グローバル翻訳関数 t() と名前衝突しないよう tm）
  const [tbd, setTbd] = React.useState(false); // 時間未定＝日付のみで保存（YYYY-MM-DD）
  if (!editable) return <InfoRow icon={icon} label={label}>{children}</InfoRow>;
  const dateOnly = (v) => /^\d{4}-\d{2}-\d{2}$/.test(String(v || ''));
  const open = () => {
    setD(value ? String(value).slice(0, 10) : today());
    setTm(value && withTime && !dateOnly(value) ? fmtTime(value) : '10:00');
    setTbd(!!(withTime && value && dateOnly(value))); // 既存が日付のみ＝時間未定でプリチェック
    setEdit(true);
  };
  const save = () => { if (!d) return; onSave(withTime && !tbd ? `${d}T${tm}` : d); setEdit(false); };
  const clear = () => { onSave(null); setEdit(false); };
  const miniInput = { ...inputStyle, padding: '5px 8px', fontSize: 12.5, width: 'auto' };
  return (
    <InfoRow icon={icon} label={label}>
      {!edit ? (
        <span onClick={open} title={t('cd.clickToEdit')}
          style={{ cursor: 'pointer', display: 'inline-flex', alignItems: 'center', gap: 7, margin: '-2px -7px', padding: '2px 7px', borderRadius: 6, transition: 'background .12s' }}
          onMouseEnter={(e) => e.currentTarget.style.background = '#f0f1f4'}
          onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
          {children}
          <Icon name="edit" size={12} stroke={2} style={{ color: '#b4bac3', flex: '0 0 auto' }} />
        </span>
      ) : (
        <div style={{ display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }}>
          <input type="date" value={d} onChange={(e) => setD(e.target.value)} style={{ ...miniInput, width: 132 }} />
          {withTime && !tbd && <input type="time" value={tm} onChange={(e) => setTm(e.target.value)} style={{ ...miniInput, width: 86 }} />}
          {withTime && <label style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: 12, color: '#6b727c', cursor: 'pointer', whiteSpace: 'nowrap' }}><input type="checkbox" checked={tbd} onChange={(e) => setTbd(e.target.checked)} style={{ accentColor: '#4a5af0' }} />{t('cd.timeTbd')}</label>}
          <IconButton name="check" size={15} title={t('btn.save')} onClick={save} />
          <IconButton name="x" size={15} title={t('btn.cancel')} onClick={() => setEdit(false)} />
          {allowClear && value && (
            <button onClick={clear} style={{ border: 'none', background: 'transparent', color: '#9aa1ab', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', textDecoration: 'underline' }}>{t('cd.setUndecided')}</button>
          )}
        </div>
      )}
    </InfoRow>
  );
}

/* 画面のどこでもはみ出さない fixed 配置のドロップダウン（カードの overflow:hidden 対策） */
function FixedDropdown({ button, children, width = 170 }) {
  const [open, setOpen] = React.useState(false);
  const [pos, setPos] = React.useState({ top: 0, left: 0 });
  const ref = React.useRef(null);
  const btnRef = React.useRef(null);
  React.useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target) && btnRef.current && !btnRef.current.contains(e.target)) setOpen(false); };
    // ドロップダウン内のスクロールでは閉じない（ページ側のスクロールのみ閉じる）
    const close = (e) => { if (ref.current && e.target && ref.current.contains(e.target)) return; setOpen(false); };
    document.addEventListener('mousedown', h);
    window.addEventListener('scroll', close, true);
    return () => { document.removeEventListener('mousedown', h); window.removeEventListener('scroll', close, true); };
  }, []);
  const toggle = (e) => {
    const r = e.currentTarget.getBoundingClientRect();
    setPos({ top: Math.min(r.bottom + 6, window.innerHeight - 280), left: Math.max(8, Math.min(r.left, window.innerWidth - width - 12)) });
    setOpen(o => !o);
  };
  return (
    <>
      <span ref={btnRef} style={{ display: 'inline-block' }}>{button(toggle, open)}</span>
      {open && (
        <div ref={ref} style={{ position: 'fixed', top: pos.top, left: pos.left, zIndex: 300, width, background: '#fff', borderRadius: 10,
          border: '1px solid #e6e8ec', boxShadow: '0 14px 34px rgba(20,22,40,.16)', padding: 6, maxHeight: 260, overflowY: 'auto' }}>
          {children(() => setOpen(false))}
        </div>
      )}
    </>
  );
}

/* 提案書/見積書タブ（汎用）：提出ステータス＋提出期限＋ファイルを1か所に集約した管理ハブ */
/* 会議記録からのAI分析（提案書の下準備）。case.proposalAnalysis に保存される */
/* 提案・見積タブ(DocTab/ProposalAnalysis) は src/case-doc.jsx へ分離 */

function DocStatusRow({ c, label, statusField, ownerField, editable = true, statuses = PROPOSAL_STATUSES, order = PROPOSAL_ORDER }) {
  const { patchCase, showToast } = useStore();
  const D = window.APP_DATA;
  const cur = statuses[c[statusField]] || statuses.none;
  const docOwner = c[ownerField] ? D.user(c[ownerField]) : null;
  if (!editable) {
    return (
      <InfoRow icon="edit" label={label}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: cur.soft, color: cur.color, borderRadius: 999, padding: '3px 11px', fontSize: 12, fontWeight: 700 }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: cur.color }} />{cur.label}
        </span>
        {docOwner && <span style={{ fontSize: 12.5, color: '#5a616c', marginLeft: 8 }}>{t('cd.ownerPrefix', { name: docOwner.name })}</span>}
      </InfoRow>
    );
  }
  const pickStatus = (key, close) => {
    patchCase(c.id, { [statusField]: key });
    showToast(t('cd.toast.statusChanged', { label, status: statuses[key].label }));
    close();
  };
  const pickOwner = (uid, close) => {
    patchCase(c.id, { [ownerField]: uid });
    showToast(uid ? t('cd.toast.ownerAssigned', { label, name: D.user(uid).name }) : t('cd.toast.ownerUnassigned', { label }));
    close();
  };
  return (
    <InfoRow icon="edit" label={label}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 7, flexWrap: 'wrap' }}>
        <FixedDropdown width={150} button={(toggle) => (
          <button onClick={toggle}
            style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: 'none', cursor: 'pointer', fontFamily: 'inherit',
              background: cur.soft, color: cur.color, borderRadius: 999, padding: '3px 11px', fontSize: 12, fontWeight: 700 }}>
            <span style={{ width: 6, height: 6, borderRadius: '50%', background: cur.color }} />
            {cur.label}
            <Icon name="chevronDown" size={12} stroke={2.2} style={{ opacity: .6 }} />
          </button>
        )}>
          {(close) => order.map(k => {
            const s = statuses[k];
            return (
              <div key={k} className="row-hover" onClick={() => pickStatus(k, close)}
                style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '7px 9px', borderRadius: 7, cursor: 'pointer' }}>
                <span style={{ width: 8, height: 8, borderRadius: '50%', background: s.color, flex: '0 0 auto' }} />
                <span style={{ fontSize: 12.5, color: '#3b414b', flex: 1 }}>{s.label}</span>
                {(c[statusField] || 'none') === k && <Icon name="check" size={14} stroke={2.4} style={{ color: '#4a5af0' }} />}
              </div>
            );
          })}
        </FixedDropdown>
        {/* 書類の担当者 */}
        <FixedDropdown width={190} button={(toggle) => (
          <button onClick={toggle} title={t('cd.docOwnerTitle', { label })}
            style={{ display: 'inline-flex', alignItems: 'center', gap: 6, border: '1px solid #e2e5ea', cursor: 'pointer', fontFamily: 'inherit',
              background: '#fff', color: docOwner ? '#2b2f38' : '#9aa1ab', borderRadius: 999, padding: '2px 10px 2px 3px', fontSize: 12, fontWeight: 600 }}>
            <Avatar user={docOwner} size={20} />
            {docOwner ? docOwner.short : t('cd.ownerShort')}
            <Icon name="chevronDown" size={11} stroke={2.2} style={{ color: '#b4bac3' }} />
          </button>
        )}>
          {(close) => (
            <>
              {D.users.map(u => (
                <div key={u.id} className="row-hover" onClick={() => pickOwner(u.id, close)}
                  style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '6px 8px', borderRadius: 7, cursor: 'pointer' }}>
                  <Avatar user={u} size={22} />
                  <span style={{ fontSize: 12.5, color: '#3b414b', flex: 1 }}>{u.name}</span>
                  {c[ownerField] === u.id && <Icon name="check" size={14} stroke={2.4} style={{ color: '#4a5af0' }} />}
                </div>
              ))}
              <div className="row-hover" onClick={() => pickOwner(null, close)}
                style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '6px 8px', borderRadius: 7, cursor: 'pointer', borderTop: '1px solid #f0f1f4', marginTop: 4 }}>
                <Avatar user={null} size={22} />
                <span style={{ fontSize: 12.5, color: '#9aa1ab' }}>{t('cd.setUnassigned')}</span>
              </div>
            </>
          )}
        </FixedDropdown>
      </div>
    </InfoRow>
  );
}

/* 会議リンク カード（Zoom/Meet/ReadyCrew 等。二次商談は上書きせず追記） */
function MeetingLinksCard({ caseData }) {
  const { addMeetingLink, removeMeetingLink } = useStore();
  // 手動登録＋本文（相談内容）から自動判別した会議URL（担当者用・参加者用の両方）
  const links = caseAllMeetingLinks(caseData);
  const [open, setOpen] = React.useState(false);
  const [label, setLabel] = React.useState('');
  const [url, setUrl] = React.useState('');
  const linkKind = (u) => /zoom/.test(u) ? { label: 'Zoom', color: '#2D8CFF' } : /meet\.google/.test(u) ? { label: 'Google Meet', color: '#16a34a' } : /teams\.microsoft/.test(u) ? { label: 'Teams', color: '#5059C9' } : /readycrew/.test(u) ? { label: 'ReadyCrew', color: '#4a5af0' } : { label: t('cd.linkType.link'), color: '#4a5af0' };
  const add = () => {
    if (!url.trim()) return;
    let u = url.trim(); if (!/^https?:\/\//.test(u)) u = 'https://' + u;
    // 既定ラベルの連番は実際に保存されている件数で数える（place は meetingPlace 由来の仮想エントリ）
    addMeetingLink(caseData.id, { label: label.trim() || t('cd.nthMeetingLabel', { n: (caseData.meetingLinks || []).length + 1 }), url: u });
    setLabel(''); setUrl(''); setOpen(false);
  };
  return (
    <Card title={t('cd.meetingLinks')} pad={16} action={<button onClick={() => setOpen(o => !o)} style={{ ...linkBtn, fontSize: 12 }}><Icon name="plus" size={13} stroke={2.2} />{t('btn.add')}</button>}>
      {links.length === 0 && !open && <div style={{ fontSize: 12.5, color: '#b4bac3' }}>{t('cd.noMeetingLinks')}</div>}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {links.map(lk => {
          const m = linkKind(lk.url);
          return (
            <div key={lk.id || lk.url} style={{ border: '1px solid #f0f1f4', borderRadius: 9, padding: '9px 11px' }}>
              {/* 1段目：アイコン＋ラベル（幅を優先）＋開く/削除 */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                <Icon name="video" size={14} stroke={2} style={{ color: m.color, flex: '0 0 auto' }} />
                <span title={lk.label} style={{ fontSize: 12.5, fontWeight: 600, color: '#2b2f38', flex: 1, minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{lk.label}</span>
                <a href={lk.url} target="_blank" rel="noreferrer" style={{ flex: '0 0 auto' }}><IconButton name="link" size={14} title={t('btn.open')} /></a>
                {lk.id && <IconButton name="x" size={14} title={t('btn.delete')} onClick={() => { if (window.confirm(t('common.confirmDelete'))) removeMeetingLink(caseData.id, lk.id); }} />}
              </div>
              {/* 2段目：種別バッジ（ラベルと取り合わず重ならない） */}
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 5, paddingLeft: 22, flexWrap: 'wrap' }}>
                {lk.place && <span style={{ fontSize: 12, fontWeight: 700, color: '#16a34a', background: '#16a34a14', padding: '1px 7px', borderRadius: 5, whiteSpace: 'nowrap' }}>{t('cd.thisTimeLink')}</span>}
                {lk.auto && <span style={{ fontSize: 12, fontWeight: 700, color: '#7c3aed', background: '#7c3aed14', padding: '1px 7px', borderRadius: 5, whiteSpace: 'nowrap' }}>{t('cd.autoDetectedLink')}</span>}
                <span style={{ fontSize: 12, fontWeight: 700, color: m.color, background: m.color + '14', padding: '1px 7px', borderRadius: 5, whiteSpace: 'nowrap' }}>{m.label}</span>
              </div>
              {lk.note && <div style={{ fontSize: 12, color: '#9aa1ab', marginTop: 4, paddingLeft: 22, fontFamily: 'var(--mono)' }}>{lk.note}</div>}
            </div>
          );
        })}
      </div>
      {open && (
        <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 7 }}>
          <TextInput value={label} onChange={(e) => setLabel(e.target.value)} placeholder={t('cd.meetingLinkLabelPlaceholder')} style={{ fontSize: 12.5 }} />
          <TextInput value={url} onChange={(e) => setUrl(e.target.value)} onKeyDown={(e) => { if (enterSubmits(e)) add(); }} placeholder={t('cd.meetingUrlPlaceholder')} style={{ fontSize: 12.5 }} />
          <div style={{ display: 'flex', gap: 6 }}>
            <Button variant="primary" size="sm" icon="plus" onClick={add} disabled={!url.trim()} full>{t('cd.addAppend')}</Button>
            <Button variant="subtle" size="sm" onClick={() => setOpen(false)}>{t('cd.cancelShort')}</Button>
          </div>
        </div>
      )}
    </Card>
  );
}

/* 社内メモ・補足（管理者は編集して保存、メンバーは読取専用） */
/* メールタブ(MailTab/OrdererReview) は src/case-mail.jsx へ分離 */

/* 成約案件→実績（ポートフォリオ）生成モーダル：AIがドラフト生成→確認・修正→ナレッジ実績へ登録。
   実績はAI提案（提案書/商談提案/エントリー文）が自動引用するため、成約のたびに貯めるほど営業が強くなる */
function AchievementDraftModal({ caseData, onClose }) {
  const { addKnowledge, showToast } = useStore();
  const [d, setD] = React.useState(null);   // AIドラフト（編集可）
  const [err, setErr] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    let alive = true;
    API.achievementDraft(caseData.id)
      .then(r => { if (alive) setD(r.draft || {}); })
      .catch(e => { if (alive) setErr(e.message || '生成に失敗しました'); });
    return () => { alive = false; };
  }, [caseData.id]);
  const set = (k) => (e) => setD(x => ({ ...x, [k]: e.target.value }));
  const inputS = { border: '1px solid #e2e5ea', borderRadius: 8, padding: '8px 10px', fontSize: 13, fontFamily: 'inherit', width: '100%', boxSizing: 'border-box', outline: 'none', color: '#1f2430' };
  const KINDS = ['受託開発', '自社サービス', '共同開発', '保守・運用', 'PoC・コンサル', 'その他'];
  const submit = async () => {
    if (!d || busy) return;
    if (!String(d.client || '').trim()) { showToast(window.t("label.extra14"), 'x'); return; }
    setBusy(true);
    const fields = [
      ['顧客・案件名', d.client], ['業種', d.industry], ['カテゴリ', (d.categories || []).join('・')], ['種別', d.kind], ['時期', d.period],
      ['内容・規模・使用技術', d.body], ['成果（数値があれば）', d.result], ['アピールポイント（提案での強み）', d.appeal], ['向いている案件・用途', d.fit],
    ].filter(f => String(f[1] || '').trim());
    const text = fields.map(f => '■' + f[0] + '\n' + String(f[1]).trim()).join('\n\n');
    try {
      await addKnowledge({ kb: 'achievement', type: 'text', title: String(d.client).trim().slice(0, 60), text, categories: d.categories || [], caseId: caseData.id });
      showToast(window.t("label.extra15"));
      onClose();
    } catch (e) { showToast(e.message || '登録に失敗しました', 'x'); setBusy(false); }
  };
  return (
    <Modal open onClose={onClose} width={640} title={window.t("extra.case.generatePortfolio")}
      subtitle={window.t("extra.case.portfolioDraftHint")}
      footer={<><Button variant="subtle" onClick={onClose}>{t('btn.cancel')}</Button><Button variant="primary" icon="check" disabled={!d || busy} onClick={submit}>{busy ? window.t("extra.common.registering") : window.t("extra.case.registerPortfolio")}</Button></>}>
      {err ? <div style={{ padding: '24px 0', textAlign: 'center', color: '#dc2626', fontSize: 13 }}>{err}</div>
        : !d ? <div style={{ padding: '30px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 13 }}>{window.t("extra.case.generatingDraft")}</div>
        : (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <Field label={window.t("extra.case.clientAndTitle")} required><input style={inputS} value={d.client || ''} onChange={set('client')} /></Field>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 160px 150px', gap: 10 }}>
            <Field label={window.t("cust2.label.industry")}><input style={inputS} value={d.industry || ''} onChange={set('industry')} /></Field>
            <Field label={window.t("cd.categoryPlaceholder")}>
              <select style={{ ...inputS, background: '#fff' }} value={d.kind || '受託開発'} onChange={set('kind')}>{KINDS.map(k => <option key={k}>{k}</option>)}</select>
            </Field>
            <Field label={window.t("extra.common.period")}><input style={inputS} value={d.period || ''} onChange={set('period')} placeholder={window.t("extra.case.dateExample")} /></Field>
          </div>
          <Field label={window.t("extra.case.contentScaleTech")}><textarea rows={4} style={{ ...inputS, resize: 'vertical', lineHeight: 1.7 }} value={d.body || ''} onChange={set('body')} /></Field>
          <Field label={window.t("extra.case.results")}><textarea rows={2} style={{ ...inputS, resize: 'vertical', lineHeight: 1.7 }} value={d.result || ''} onChange={set('result')} /></Field>
          <Field label={window.t("extra.case.strengths")}><textarea rows={2} style={{ ...inputS, resize: 'vertical', lineHeight: 1.7 }} value={d.appeal || ''} onChange={set('appeal')} /></Field>
          <Field label={window.t("extra.case.suitableUses")}><input style={inputS} value={d.fit || ''} onChange={set('fit')} /></Field>
          {(d.categories || []).length > 0 && <div style={{ fontSize: 11.5, color: '#9aa1ab' }}>{window.t("extra.common.categoryPrefix")}{d.categories.join('・')}{window.t("extra.case.inherited")}</div>}
        </div>
      )}
    </Modal>
  );
}

/* 次の一手（ToDo）カード：案件詳細の左カラム。商談後のAI提案／手動で積んだアクションを
   完了トグル・削除・手動追加で管理。ダッシュボードの ToDo と同じ case.nextActions が源。 */
function NextActionsCard({ caseData }) {
  const { toggleNextAction, removeNextAction, addNextActions } = useStore();
  const [txt, setTxt] = React.useState('');
  const [naType, setNaType] = React.useState('call');
  const [due, setDue] = React.useState('');
  const list = (caseData.nextActions || []).slice().sort((a, b) => (a.done ? 1 : 0) - (b.done ? 1 : 0) || String(a.due || '9999').localeCompare(String(b.due || '9999')));
  const openN = list.filter(a => !a.done).length;
  const add = () => { if (!txt.trim()) return; addNextActions(caseData.id, [{ text: txt.trim(), type: naType, due: due || null }]); setTxt(''); setDue(''); };
  return (
    <Card title={t('na.card.title') + (openN ? '（' + openN + '）' : '')} pad={16}>
      {list.length === 0
        ? <div style={{ fontSize: 12.5, color: '#b4bac3', lineHeight: 1.6 }}>{t('na.noItems')}</div>
        : <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            {list.map(a => {
              const meta = naTypeMeta(a.type);
              const overdue = !a.done && a.due && daysUntil(a.due) < 0;
              return (
                <div key={a.id} style={{ display: 'flex', gap: 9, alignItems: 'flex-start' }}>
                  <button onClick={() => toggleNextAction(caseData.id, a.id)} title={t('na.done')}
                    style={{ width: 18, height: 18, marginTop: 1, borderRadius: 5, border: '1.5px solid ' + (a.done ? '#4a5af0' : '#cfd4db'), background: a.done ? '#4a5af0' : '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flex: '0 0 auto', padding: 0 }}>
                    {a.done && <Icon name="check" size={12} stroke={3} style={{ color: '#fff' }} />}
                  </button>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
                      <span style={{ fontSize: 9.5, fontWeight: 700, color: meta.color, background: meta.bg, padding: '1px 6px', borderRadius: 999 }}>{meta.label}</span>
                      <span style={{ fontSize: 12.5, color: a.done ? '#b4bac3' : '#2b2f38', textDecoration: a.done ? 'line-through' : 'none' }}>{a.text}</span>
                    </div>
                    {a.detail && !a.done && <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 2, lineHeight: 1.5 }}>{a.detail}</div>}
                    {a.due && <div style={{ fontSize: 11, color: overdue ? '#c0392b' : '#9aa1ab', marginTop: 2, fontWeight: overdue ? 700 : 400 }}>{fmtDate(a.due)}{overdue ? ' ' + t('cust2.due') + window.t("extra.common.overdue") : ''}</div>}
                  </div>
                  <button onClick={() => removeNextAction(caseData.id, a.id)} title={t('btn.delete')} style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: '#cbd0d7', padding: 2, flex: '0 0 auto' }}><Icon name="x" size={13} stroke={2.4} /></button>
                </div>
              );
            })}
          </div>}
      <div style={{ marginTop: list.length ? 11 : 9, paddingTop: list.length ? 11 : 0, borderTop: list.length ? '1px solid #f4f5f7' : 'none' }}>
        <input value={txt} onChange={e => setTxt(e.target.value)} onKeyDown={e => { if (enterSubmits(e)) add(); }} placeholder={t('na.placeholder')}
          style={{ ...inputStyle, fontSize: 12.5, padding: '7px 10px', width: '100%', boxSizing: 'border-box' }} />
        <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
          <select value={naType} onChange={e => setNaType(e.target.value)}
            style={{ ...inputStyle, fontSize: 12, padding: '6px 8px', width: 88, flex: '0 0 auto', cursor: 'pointer' }}>
            {['call', 'mail', 'schedule', 'quote', 'proposal', 'doc', 'internal', 'other'].map(k => <option key={k} value={k}>{t('na.type.' + k)}</option>)}
          </select>
          <input type="date" value={due} onChange={e => setDue(e.target.value)}
            style={{ ...inputStyle, fontSize: 12, padding: '6px 8px', flex: 1, minWidth: 0 }} />
          <Button size="sm" variant="default" icon="plus" onClick={add}>{t('btn.add')}</Button>
        </div>
      </div>
    </Card>
  );
}

/* メール往復から「次回日程の候補」を検出（2回目以降のアポをカレンダーへ取りこぼさないための自動読取）。
   直近のメール（新しい順・最大8通）の件名＋本文から 7/15・7月15日(水) 14:00/14時 等の未来日時を抽出し、
   最も新しいメールの候補を返す。手段（電話/オンライン/訪問）も文脈から推定。確定はユーザーの1クリック。 */
function cdMailNextCandidate(mails, currentDt) {
  const now = new Date();
  const list = (mails || []).slice(0, 8);
  for (const m of list) {
    const text = ((m.subject || '') + '\n' + (m.body || m.snippet || '')).slice(0, 4000);
    // 曜日括弧は （月）(月)〈月〉<月>《月》［月］[月]【月】＋「月曜(日)」表記まで許容。時刻の区切りは : ： 時
    // （〈月〉を想定しておらず「7月13日〈月〉14:00」の時刻が拾えず10:00仮置きになったバグの修正・2026-07-08）
    const re = /(\d{1,2})\s*[\/月]\s*(\d{1,2})日?\s*(?:[（(〈<《［[【]\s*[月火水木金土日](?:曜日?)?\s*[）)〉>》］\]】])?\s*(?:の)?\s*(午前|午後)?\s*(?:(\d{1,2})\s*[:：時]\s*(\d{2})?)?/g;
    let mt; const cands = [];
    while ((mt = re.exec(text))) {
      const mo = +mt[1], da = +mt[2];
      if (mo < 1 || mo > 12 || da < 1 || da > 31) continue;
      let hh = mt[4] != null ? +mt[4] : null; const mi = mt[5] != null ? +mt[5] : 0;
      if (hh != null && mt[3] === '午後' && hh < 12) hh += 12;
      if (hh != null && (hh < 0 || hh > 23)) continue;
      let y = now.getFullYear();
      const d = new Date(y, mo - 1, da, hh == null ? 10 : hh, mi);
      if (d.getTime() < now.getTime() - 86400000 * 2) { y += 1; d.setFullYear(y); } // 過去日→来年扱い（年またぎ）
      if (d.getTime() < now.getTime()) continue; // それでも過去はスキップ
      if (d.getTime() > now.getTime() + 86400000 * 180) continue; // 半年以上先は誤検出扱い
      const p = (n) => String(n).padStart(2, '0');
      const dt = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
      const around = text.slice(Math.max(0, mt.index - 40), mt.index + 60);
      cands.push({ dt, hasTime: hh != null, around });
    }
    if (!cands.length) continue;
    cands.sort((a, b) => (b.hasTime ? 1 : 0) - (a.hasTime ? 1 : 0)); // 時刻ありを優先
    const c0 = cands[0];
    if (currentDt && String(currentDt).slice(0, 16) === c0.dt) return null; // 既に同じ日時が登録済み
    const method = /電話|架電|お電話|ＴＥＬ|TEL/i.test(text) ? 'phone' : /訪問|来社|お伺い|ご来社|往訪/.test(text) ? 'visit' : /zoom|meet|teams|オンライン|web会議|ウェブ会議/i.test(text) ? 'online' : 'online';
    return { dt: c0.dt, hasTime: c0.hasTime, method, from: m.direction === 'in' ? (m.fromName || m.from || '') : '', subject: (m.subject || '').slice(0, 40), snippet: c0.around.replace(/\s+/g, ' ').trim().slice(0, 60) };
  }
  return null;
}

function CaseDetail() {
  const { route, navigate, cases, toggleSub, assignOwner, claimCase, meetingsOf, logsOf, saveCase, removeCase, scheduleMeeting, updateMeeting, removeMeeting, patchCase, setCategories, currentUser, emailsOf, can, showToast, bodyDupMap, addMeetingLink } = useStore();
  const isMobile = useIsMobile();
  const canEdit = can('caseEdit'); // 案件の編集・期限・メモ（権限設定で制御）
  const canDelete = can('caseDelete'); // 案件の削除（既定で管理者のみ）
  const delCase = async () => {
    if (!c) return;
    if (!window.confirm((c.title || '') + '\n\n' + t('cd.confirmDeleteCase'))) return;
    try { await removeCase(c.id); navigate('cases'); }
    catch (e) { showToast(e.message || t('cd.deleteFailed'), 'x'); }
  };
  const D = window.APP_DATA;
  const c = cases.find(k => k.id === route.id);
  // カテゴリ候補＝マスタ候補 ∪ 既存案件で使用中の値（自由入力した独自カテゴリも次回から選択肢に出る）
  const catOptions = Array.from(new Set([...(D.CATEGORIES || []), ...cases.flatMap(k => k.categories || [])]));
  const TAB_KEYS = ['meetings', 'content', 'proposal', 'quote', 'prototype', 'attach', 'meetingDocs', 'mail', 'logs']; // shodan(商談提案)はmeetingsタブに統合（上=提案/下=履歴）
  const [tab, setTab] = React.useState(() => TAB_KEYS.includes(route.tab) ? route.tab : 'meetings');
  // URL ハッシュのタブ（共有リンク・戻る/進む）を内部 state に反映
  React.useEffect(() => {
    const want = TAB_KEYS.includes(route.tab) ? route.tab : 'meetings';
    if (want !== tab) setTab(want);
  }, [route.tab]);
  // タブ切替＝内部 state ＋ URL を更新（#/case/<id>/<tab>）
  const selectTab = (k) => { setTab(k); navigate('case', route.id, k); };
  const [openMeeting, setOpenMeeting] = React.useState(false);
  const [openEdit, setOpenEdit] = React.useState(false);
  const [achOpen, setAchOpen] = React.useState(false); // 成約実績の生成（成約案件のみ）
  const [mailCandDismiss, setMailCandDismiss] = React.useState(''); // メール由来の次回候補バナーを閉じた印（案件+日時キー）
  const [assignOpen, setAssignOpen] = React.useState(false);
  const assignRef = React.useRef(null);
  React.useEffect(() => { const h = (e) => { if (assignRef.current && !assignRef.current.contains(e.target)) setAssignOpen(false); };
    document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, []);
  if (!c) return <Page title={t('page.case')}><div>{t('cd.caseNotFound')}</div></Page>;
  const cust = D.customer(c.customerId) || {}; // 顧客が削除済み等で見つからない場合に画面全体が落ちるのを防ぐ
  const owner = c.ownerId ? D.user(c.ownerId) : null;
  const subs = c.subIds.map(id => D.user(id));
  // 商談履歴 = 手入力の商談記録 + 会議記録（Fireflies/手動録画）。同日は手入力を優先
  const rawMtgs = meetingsOf(c.id);
  const docMtgs = (c.meetingDocs || [])
    .filter(d => d.datetime && !rawMtgs.some(m => (m.datetime || '').slice(0, 10) === d.datetime.slice(0, 10)))
    .map(d => ({
      id: 'mdoc-' + d.id, datetime: d.datetime, method: 'online', authorId: c.ownerId,
      summary: (d.bullets && d.bullets[0]) || d.summary || (d.aiSummary || '').split('\n').find(s => s && !s.startsWith('■')) || (d.title ? t('cd.meetingDocSummary', { title: d.title }) : t('cd.meetingNoSummary')),
      nextAction: (d.actions && d.actions[0] && d.actions[0].text) || null,
      customerFeedback: d.customerFeedback || '',
      source: d.manual ? 'manual' : 'fireflies', isDoc: true, docId: d.id, probability: (d.probability != null ? d.probability : null),
    }));
  // 予定（未来の商談日：次回商談・ReadyCrew商談日・Googleカレンダー）も「予定」として履歴に出す
  const recordedDays = new Set([...rawMtgs, ...docMtgs].map(m => (m.datetime || '').slice(0, 10)));
  const normCoH = (s) => (s || '').replace(/株式会社|（株）|\(株\)|・(レディクル|発注ナビ).*$|[（(].*$|\s+/g, '');
  const coCore = cust ? normCoH(cust.company) : '';
  const planRows = [];
  const addPlan = (dt, gcal, method) => {
    if (!dt) return;
    const day = dt.slice(0, 10);
    if (day < today() || recordedDays.has(day) || planRows.some(p => p.datetime.slice(0, 10) === day)) return;
    planRows.push({ id: 'plan-' + day, datetime: dt, method: method || 'online', authorId: c.ownerId, isPlan: true, gcal: gcal || null });
  };
  // Googleカレンダーの予定（Meet・参加者付き）を最優先。次に次回商談・ReadyCrew商談日
  // gcalPlanDedupe＝同じ「n回目」を名乗る未来予定の重複除去＋Meet付き先頭（同日1件＝先勝ちの既存ルールと整合）
  gcalPlanDedupe((D.gcalEvents || []).filter(g => (coCore.length >= 3 && normCoH(g.title).includes(coCore)) || (coCore.length >= 2 && normCoH(g.title) === coCore)))
    .forEach(g => addPlan(g.datetime, g));
  // 次回商談とRC商談日が両方未来のときは casePlanPick が正を一つに絞る（二重表示防止）
  const planPick = casePlanPick(c);
  if (planPick.nm) addPlan(c.nextMeeting, null, caseNextMethod(c));
  if (planPick.ap) addPlan(c.appointAt, null, caseNextMethod(c));
  const mtgs = [...rawMtgs, ...docMtgs, ...planRows].sort((a, b) => (b.datetime || '').localeCompare(a.datetime || ''));
  const logs = logsOf(c.id);
  const methodIcon = { visit: 'mapPin', phone: 'phone', online: 'video', email: 'mail' };

  const tabs = [
    { k: 'meetings', label: `${t('tab.meetings')} (${mtgs.length})` },
    { k: 'content', label: t('tab.content') },
    { k: 'proposal', label: `${t('cd.tab.proposal')}${(c.attachments || []).filter(a => a.category === '提案書').length ? ` (${(c.attachments || []).filter(a => a.category === '提案書').length})` : ''}` },
    { k: 'quote', label: `${t('cd.tab.quote')}${(c.attachments || []).filter(a => a.category === '見積書').length ? ` (${(c.attachments || []).filter(a => a.category === '見積書').length})` : ''}` },
    { k: 'prototype', label: `${t('cd.tab.prototype')}${(c.attachments || []).filter(a => a.category === 'プロトタイプ').length ? ` (${(c.attachments || []).filter(a => a.category === 'プロトタイプ').length})` : ''}` },
    { k: 'attach', label: t('tab.attach') },
    { k: 'meetingDocs', label: `${t('tab.meetingDocs')}${(c.meetingDocs || []).length ? ` (${c.meetingDocs.length})` : ''}` },
    { k: 'mail', label: `${t('tab.mail')}${emailsOf(c.customerId).length ? ` (${emailsOf(c.customerId).length})` : ''}` },
    { k: 'logs', label: t('tab.logs') },
  ];

  // 段階連動の「次の一歩」ナビ（④）：状態・商談実施・提案/見積・放置から次の最善アクションを1つだけ提示。
  // 既存の「次の一手」(nextActions ToDo) とは別＝こちらは自動判定の単発サジェスト。優先度＝未割当>放置>未商談>提案>見積>クロージング>予定あり。
  const nextStep = (() => {
    if (['won', 'lost', 'done'].includes(c.status)) return null;
    const cnt = caseMeetingCount(c);
    const up = c.nextMeeting || c.appointAt;
    const hasUpcoming = up && daysUntil(up) >= 0;
    const propDone = c.proposalStatus === 'submitted' || c.proposalStatus === 'na';
    const quoDone = c.quoteStatus === 'submitted' || c.quoteStatus === 'na';
    const idle = caseIdleInfo(c);
    if (!c.ownerId) return { msg: '担当者が未割当です。まず担当を決めましょう。', get label(){return window.t("label.extra16");}, tone: 'warn', act: () => claimCase(c.id) };
    if (idle) return { msg: idle.idleDays + '日 動きがありません。フォロー連絡か次回商談を設定しましょう。', get label(){return window.t("label.extra17");}, tone: 'warn', act: () => setTab('meetings') };
    if (cnt === 0 && !hasUpcoming) return { msg: 'まだ商談していません。初回商談を設定しましょう。', get label(){return window.t("label.extra18");}, tone: 'accent', act: () => setTab('meetings') };
    if (cnt >= 1 && !propDone) return { msg: '商談済みです。次は提案を出しましょう。', get label(){return window.t("label.extra19");}, tone: 'accent', act: () => setTab('proposal') };
    if (propDone && !quoDone) return { msg: '提案済みです。次は見積を出しましょう。', get label(){return window.t("label.extra20");}, tone: 'accent', act: () => setTab('quote') };
    if (propDone && quoDone && !hasUpcoming) return { msg: '提案・見積は提出済み。クロージングの商談を設定しましょう。', get label(){return window.t("label.extra18");}, tone: 'accent', act: () => setTab('meetings') };
    if (hasUpcoming) return { msg: '次回商談 ' + fmtDateFull(up) + ' が予定されています。準備しましょう。', get label(){return window.t("label.extra21");}, tone: 'info', act: () => setTab('meetings') };
    return null;
  })();
  const nsTone = { warn: { bg: '#fdf0db', bd: '#f2d9a8', tx: '#92500e', dot: '#b45309' }, accent: { bg: '#eef0fe', bd: '#d9d6fb', tx: '#4a45b5', dot: '#4a5af0' }, info: { bg: '#e8f0fe', bd: '#c9dbf5', tx: '#1d4ed8', dot: '#2563eb' } };

  return (
    <Page title={t('page.case')} right={<>{c.status === 'won' && <Button variant="default" icon="chart" onClick={() => setAchOpen(true)} title={window.t("extra.case.portfolioHint")}>{window.t("extra.case.generateAchievement")}</Button>}{canEdit && <Button variant="default" icon="edit" onClick={() => setOpenEdit(true)}>{t('btn.edit')}</Button>}<StatusMenu caseId={c.id} current={c.status} archived={c.archived} />{canDelete && <Button variant="subtle" icon="x" onClick={delCase} style={{ color: '#dc2626' }}>{t('btn.delete')}</Button>}</>}>
      {/* パンくず */}
      <button onClick={() => navigate('cases')} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, border: 'none', background: 'transparent', color: '#9aa1ab', fontSize: 12.5, cursor: 'pointer', fontFamily: 'inherit', marginBottom: 14 }}>
        <Icon name="chevronLeft" size={14} stroke={2} />{t('cd.backToCases')}
      </button>

      {/* ヘッダ */}
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, marginBottom: 18 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
            <h1 style={{ fontSize: 22, fontWeight: 700, color: '#1c1f26', margin: 0 }}>{c.title}</h1>
            <StatusBadge status={c.status} />
            {c.archived && <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 12, fontWeight: 700, color: '#6b727c', background: '#eef0f3', padding: '3px 10px', borderRadius: 999 }}><Icon name="inbox" size={13} stroke={2} />{t('cases.archived')}</span>}
            <RankSelect caseId={c.id} size="lg" />
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: 9, fontSize: 12, color: '#9aa1ab', flexWrap: 'wrap' }}>
            <span style={{ fontFamily: 'var(--mono)' }}>{c.sourceNo}</span>
            {caseViaMeta(c).key !== 'manual' ? (
              <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}><Icon name="link" size={13} stroke={2} />{c.sourceUrl ? <a href={c.sourceUrl} target="_blank" rel="noopener noreferrer" style={{ color: '#4a5af0', textDecoration: 'none' }}>{t('cd.sourceUrl')}</a> : <span style={{ color: '#9aa1ab' }}>{t('cd.sourceUrl')}</span>}</span>
            ) : <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}><Icon name="edit" size={12} stroke={2} />{t('cd.manualEntry')}</span>}
            {caseViaMeta(c).key !== 'manual' && <span style={{color:caseViaMeta(c).color}}>{caseViaMeta(c).label}</span>}
            <span>{t('cd.acquiredLabel')} <span style={{ color: '#4a5af0' }}>{fmtDateTime(c.createdAt)}</span></span>
            <span style={{ display: c.source === 'scrape' ? 'inline-flex' : 'none', alignItems: 'center', gap: 5, background: '#eef0fe', color: '#4a5af0', padding: '2px 8px', borderRadius: 5, fontWeight: 600 }}>
              <Icon name="refresh" size={11} stroke={2.2} />{t('cases.autoImported')}
            </span>
          </div>
        </div>
      </div>

      {/* 次の一歩ナビ（④）：段階に応じた次の最善アクションを1つ提示。押すと該当タブ/操作へ */}
      {nextStep && (() => { const tn = nsTone[nextStep.tone]; return (
        <div style={{ display: 'flex', alignItems: 'center', gap: 11, padding: '11px 15px', borderRadius: 12, marginBottom: 16, background: tn.bg, border: '1px solid ' + tn.bd }}>
          <Icon name="spark" size={16} stroke={2} style={{ color: tn.dot, flex: '0 0 auto' }} />
          <div style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600, color: tn.tx }}><span style={{ fontWeight: 800 }}>{window.t("extra.case.nextStep")}</span>{nextStep.msg}</div>
          <Button size="sm" variant="primary" onClick={nextStep.act}>{nextStep.label}</Button>
        </div>
      ); })()}

      <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '300px 1fr', gap: isMobile ? 14 : 18, alignItems: 'start' }}>
        {/* 左カラム */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <Card pad={16} style={{ overflow: 'visible' }}>
            <WorkspaceFirefliesImport caseData={c}/><CaseWorkspaceField caseData={c}/>
            {/* 担当 */}
            <div style={{ marginBottom: 4 }}>
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 9 }}>
                <div style={{ fontSize: 12, color: '#9aa1ab', fontWeight: 600 }}>{t('cd.assignee')}</div>
                <div ref={assignRef} style={{ position: 'relative' }}>
                  {can('assign') && <button onClick={() => setAssignOpen(o => !o)} style={{ ...linkBtn, fontSize: 12 }}>{t('cd.change')}</button>}
                  {assignOpen && <AssignPopover ownerId={c.ownerId} subIds={c.subIds}
                    onPick={async (uid) => { await assignOwner(c.id, uid); setAssignOpen(false); }}
                    onToggleSub={(uid) => toggleSub(c.id, uid)}
                    onClose={() => setAssignOpen(false)} anchorStyle={{ top: 26, right: 0 }} />}
                </div>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                <Avatar user={owner} size={36} />
                <div>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: '#1f2430', whiteSpace: 'nowrap' }}>{owner ? owner.name : t('cd.unassigned')}</div>
                  <div style={{ fontSize: 12, color: '#9aa1ab' }}>{t('case-form.mainOwner')}</div>
                </div>
                {!c.ownerId && <button onClick={() => claimCase(c.id)} style={{ marginLeft: 'auto', background: '#4a5af0', color: '#fff', border: 'none', borderRadius: 8, padding: '7px 12px', fontSize: 12.5, fontWeight: 700, cursor: 'pointer', whiteSpace: 'nowrap' }}>{t('cd.claimSelf')}</button>}
              </div>
              {subs.length > 0 && (
                <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span style={{ fontSize: 12, color: '#9aa1ab', fontWeight: 600 }}>{t('case-form.subOwner')}</span>
                  {subs.map(s => (
                    <span key={s.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, background: '#f4f5f7', borderRadius: 999, padding: '3px 9px 3px 3px' }}>
                      <Avatar user={s} size={20} /><span style={{ fontSize: 12, color: '#3b414b', fontWeight: 500 }}>{s.short}</span>
                    </span>
                  ))}
                </div>
              )}
            </div>
            <div style={{ height: 1, background: '#f0f1f4', margin: '14px 0' }} />
            <EditableDateRow icon="clock" label={t('cd.due')} value={c.due} onSave={(v) => saveCase({ id: c.id, due: v })} editable={canEdit}>
              {(() => {
                const due = caseDue(c);
                const ddu = daysUntil(due);
                return due ? (
                  <>
                    <span style={{ fontWeight: 600, color: ddu !== null && ddu <= 3 && !['won', 'lost', 'done'].includes(c.status) ? '#dc2626' : '#2b2f38' }}>{fmtDateFull(due)}</span>
                    {ddu !== null && !['won', 'lost', 'done'].includes(c.status) && <span style={{ fontSize: 12, color: '#9aa1ab', marginLeft: 8 }}>{ddu <= 0 ? t('cd.daysOverdue', { n: -ddu }) : t('cd.daysRemaining', { n: ddu })}</span>}
                    {!c.due && <span style={{ fontSize: 12, color: '#a8aeb8', marginLeft: 8 }}>{t('cd.dueAutoNote')}</span>}
                  </>
                ) : <span style={{ color: '#9aa1ab' }}>{t('cd.undecided')}</span>;
              })()}
            </EditableDateRow>
            <EditableDateRow icon="calendar" label={t('cd.nextMeeting')} value={c.nextMeeting || c.appointAt} withTime allowClear onSave={(v) => scheduleMeeting(c.id, v)} editable={true}>
              {(() => {
                const mt = caseMeetingAt(c);
                return <span style={{ color: mt ? '#2b2f38' : '#9aa1ab' }}>{mt ? `${fmtDateFull(mt)} ${fmtTime(mt) || t('cd.timeTbd')}` : t('cd.undecided')}</span>;
              })()}
            </EditableDateRow>
            {/* 受注金額（成約金額）＝成約/受注案件のみ。KPI・顧客集計に反映（未入力は見積合計） */}
            {isCaseWonStatus(c.status) && <WonAmountRow c={c} saveCase={saveCase} canEdit={canEdit} />}
            {/* 受注理由＝成約/受注案件のみ。変更時モーダルで未記入でも後追い編集できる */}
            {isCaseWonStatus(c.status) && <WonReasonRow c={c} canEdit={canEdit} />}
            {/* 失注理由＝失注案件のみ。ステータス変更時のモーダルで未記入でも、ここから後追い編集できる */}
            {c.status === 'lost' && <LostReasonRow c={c} canEdit={canEdit} />}
            {/* メール往復から次回日程の候補を自動検出 → 1クリックでカレンダー（次回商談）へ反映 */}
            {(() => {
              const cand = cdMailNextCandidate(emailsOf(c.customerId), c.nextMeeting || c.appointAt);
              if (!cand || mailCandDismiss === c.id + cand.dt) return null;
              const mLb = cand.method === 'phone' ? ((D.METHODS.phone || {}).label || '電話') : cand.method === 'visit' ? ((D.METHODS.visit || {}).label || '訪問') : t('cd.methodOnline');
              return (
                <div style={{ margin: '2px 0 10px', padding: '10px 12px', background: '#f4f9ff', border: '1px dashed #a8c8f0', borderRadius: 10 }}>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, fontWeight: 700, color: '#1d4ed8' }}>
                    <Icon name="mail" size={13} stroke={2.2} />{t('cd.mailCand.title')}
                    <button onClick={() => setMailCandDismiss(c.id + cand.dt)} style={{ marginLeft: 'auto', border: 'none', background: 'transparent', color: '#9aa1ab', cursor: 'pointer', padding: 0, display: 'inline-flex' }}><Icon name="x" size={13} stroke={2.2} /></button>
                  </div>
                  <div style={{ fontSize: 13, fontWeight: 700, color: '#1f2430', marginTop: 5 }}>{fmtDateFull(cand.dt)} {fmtTime(cand.dt)} <span style={{ fontSize: 11, fontWeight: 700, color: '#4a5af0', background: '#eef0fe', padding: '1px 7px', borderRadius: 999, marginLeft: 4 }}>{mLb}</span>{!cand.hasTime && <span style={{ fontSize: 10.5, color: '#9aa1ab', marginLeft: 6 }}>{t('cd.mailCand.timeAssumed')}</span>}</div>
                  {cand.snippet && <div style={{ fontSize: 11.5, color: '#7b828d', marginTop: 4, lineHeight: 1.5 }}>「…{cand.snippet}…」</div>}
                  <div style={{ marginTop: 8 }}>
                    <Button size="sm" variant="primary" icon={cand.method === 'phone' ? 'phone' : 'calendar'} onClick={() => { scheduleMeeting(c.id, cand.dt, cand.method); setMailCandDismiss(c.id + cand.dt); }}>{cand.method === 'phone' ? t('cd.mailCand.registerTodo') : t('cd.mailCand.register')}</Button>
                  </div>
                </div>
              );
            })()}
            {/* 会議URL・場所 →（変更すると）Googleカレンダーの場所欄へ反映 の順に並べる */}
            <MeetingPlaceRow c={c} saveCase={saveCase} canEdit={canEdit} />
            <CalInviteRow c={c} />
            <InfoRow icon="history" label={t('cases.col.meetings')}>
              <span style={{ fontWeight: 600, color: '#2b2f38' }}>{t('cd.timesCount', { n: caseMeetingCount(c) })}</span>
            </InfoRow>
            <InfoRow icon="cases" label={t('cd.category')}>
              <CategoryTags value={c.categories} onChange={(cats) => setCategories(c.id, cats)} editable={canEdit} size="sm" options={catOptions} />
            </InfoRow>
            {/* 書類グループ（提案書・見積書・プロトタイプ）— メールグループと区切って密集を緩和 */}
            <div style={{ height: 1, background: '#f0f1f4', margin: '12px 0 0' }} />
            <div style={{ fontSize: 11, fontWeight: 700, color: '#a8aeb8', letterSpacing: '.04em', padding: '9px 0 1px' }}>{t('cd.docsGroup')}</div>
            <DocStatusRow c={c} label={t('cd.proposalSubmission')} statusField="proposalStatus" ownerField="proposalOwnerId" />
            <DocStatusRow c={c} label={t('cd.quoteSubmission')} statusField="quoteStatus" ownerField="quoteOwnerId" />
            <DocStatusRow c={c} label={t('cd.tab.prototype')} statusField="prototypeStatus" ownerField="prototypeOwnerId" editable={can('prototypeEdit')} />
            {/* メールグループ（お礼・提案メール・期限） */}
            <div style={{ height: 1, background: '#f0f1f4', margin: '12px 0 0' }} />
            <div style={{ fontSize: 11, fontWeight: 700, color: '#a8aeb8', letterSpacing: '.04em', padding: '9px 0 1px' }}>{t('cd.mailGroup')}</div>
            <DocStatusRow c={c} label={t('cd.thanksMail')} statusField="thanksMailStatus" ownerField="thanksMailOwnerId" statuses={MAIL_STATUSES} order={MAIL_ORDER} />
            <DocStatusRow c={c} label={t('cd.proposalMail')} statusField="proposalMailStatus" ownerField="proposalMailOwnerId" statuses={MAIL_STATUSES} order={MAIL_ORDER} />
            <EditableDateRow icon="clock" label={t('cd.proposalMailDue')} value={c.proposalMailDue} allowClear onSave={(v) => saveCase({ id: c.id, proposalMailDue: v })} editable={canEdit}>
              {(() => { const md = caseMailDue(c); return md ? (<><span style={{ fontWeight: 600, color: '#2b2f38' }}>{fmtDateFull(md)}</span>{!c.proposalMailDue && <span style={{ fontSize: 12, color: '#a8aeb8', marginLeft: 8 }}>{t('cd.mailDueFromMeeting7')}</span>}</>) : <span style={{ color: '#9aa1ab' }}>{t('cd.undecided')}</span>; })()}
            </EditableDateRow>
          </Card>

          {/* 相手先情報 */}
          <Card title={t('cd.partnerInfo')} pad={16} action={<button onClick={() => navigate('customer', c.customerId)} style={{ ...linkBtn, fontSize: 12 }}>{t('cd.customerDetail')} <Icon name="arrowRight" size={12} stroke={2} /></button>}>
            <div onClick={() => navigate('customer', c.customerId)} style={{ cursor: 'pointer', marginBottom: 4 }}>
              <div style={{ fontSize: 14, fontWeight: 700, color: '#1f2430' }}>{cust.company}</div>
              <div style={{ fontSize: 12, color: '#9aa1ab', marginTop: 2 }}>{cust.shortName}</div>
            </div>
            <div style={{ height: 1, background: '#f0f1f4', margin: '10px 0 4px' }} />
            <InfoRow icon="user" label={t('cust.col.contact')}><div>{cust.contact}{cust.contactGender ? `（${cust.contactGender}）` : ''}</div>{cust.contactDept ? <div style={{ color: '#9aa1ab', fontSize: 12, marginTop: 2 }}>{cust.contactDept}</div> : null}</InfoRow>
            <InfoRow icon="phone" label={t('cd.tel')}>{cust.tel}{cust.contactTel ? <div style={{ color: '#9aa1ab', fontSize: 12, marginTop: 2 }}>{window.t("extra.case.directPhone")}{cust.contactTel}</div> : null}</InfoRow>
            <InfoRow icon="mail" label="Email"><a href={'mailto:' + cust.email} style={{ color: '#4a5af0', textDecoration: 'none' }}>{cust.email}</a></InfoRow>
            {cust.url && (/^https?:\/\//.test(cust.url) || /[\w-]+\.[\w-]{2,}/.test(cust.url)) ? <InfoRow icon="globe" label={t('cd.website')}><a href={/^https?:\/\//.test(cust.url) ? cust.url : 'https://' + cust.url} target="_blank" rel="noreferrer" style={{ color: '#4a5af0', textDecoration: 'none', wordBreak: 'break-all' }}>{cust.url.replace(/^https?:\/\//, '')}</a></InfoRow> : null}
            <InfoRow icon="mapPin" label={t('cd.location')}>{cust.address}</InfoRow>
            {cust.capital && <InfoRow icon="chart" label={t('cust2.label.capital')}>{cust.capital}</InfoRow>}
            {cust.sales && <InfoRow icon="chart" label={t('cust2.label.sales')}>{cust.sales}</InfoRow>}
            {cust.contactNote && <div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid #f0f1f4' }}>
              <div style={{ fontSize: 11, fontWeight: 700, color: '#9aa1ab', marginBottom: 5 }}>{t('cust2.label.contactNote')}</div>
              <div style={{ fontSize: 12, color: '#3b414b', lineHeight: 1.65, whiteSpace: 'pre-wrap' }}>{cust.contactNote}</div>
            </div>}
            {/* ReadyCrew案件で担当者・連絡先が空＝一覧取込のみで商談詳細ページ未訪問。
               担当者名・電話・メールはRCの商談詳細ページにしか出ないため、開けば自動取込される旨を案内 */}
            {c.source === 'scrape' && c.sourceUrl && !String(cust.contact || '').trim() && !String(cust.tel || '').trim() && !String(cust.email || '').trim() && (
              <div style={{ marginTop: 10, padding: '9px 11px', background: '#fffbeb', border: '1px solid #fde68a', borderRadius: 9, fontSize: 12, color: '#92400e', lineHeight: 1.6 }}>
                {t('cd.rcContactHint')}<br />
                <a href={c.sourceUrl} target="_blank" rel="noreferrer" style={{ color: '#b45309', fontWeight: 700 }}>{t('cd.rcContactHintLink')}</a>
              </div>
            )}
          </Card>

          {/* 会議リンク（二次商談は追記） */}
          <MeetingLinksCard caseData={c} />

          {/* メモ */}
          <Card title={t('cd.memo')} pad={16}>
            <div style={{ fontSize: 13, color: c.note ? '#3b414b' : '#b4bac3', lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>{c.note || t('cd.noMemo')}</div>
          </Card>

          {/* 次の一手（ToDo）：商談後のAI提案／手動アクションの管理 */}
          <NextActionsCard caseData={c} />
        </div>

        {/* 右カラム（タブ） */}
        <Card pad={0}>
          <div style={{ display: 'flex', gap: 2, padding: '0 16px', borderBottom: '1px solid #f0f1f4', overflowX: 'auto' }}>
            {tabs.map(t => (
              <button key={t.k} onClick={() => selectTab(t.k)} style={{ padding: '14px 12px 12px', border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: 'inherit', flex: '0 0 auto', whiteSpace: 'nowrap',
                fontSize: 13, fontWeight: 600, color: tab === t.k ? '#1c1f26' : '#9aa1ab', borderBottom: '2px solid ' + (tab === t.k ? '#4a5af0' : 'transparent'), marginBottom: -1, transition: 'color .12s' }}>
                {t.label}
              </button>
            ))}
          </div>

          {tab === 'meetings' && (
            <div style={{ padding: 20 }}>
              {/* 失注/受注理由の大型カード（失注・成約案件のみ）＝サイドバーの小行より書きやすい主導線 */}
              {c.status === 'lost' && <CaseReasonCard c={c} kind="lost" canEdit={canEdit} />}
              {isCaseWonStatus(c.status) && <CaseReasonCard c={c} kind="won" canEdit={canEdit} />}
              {/* 上：商談提案（次の商談プレイブックAI） */}
              <div style={{ fontSize: 13.5, fontWeight: 700, color: '#1f2430', marginBottom: 12, display: 'flex', alignItems: 'center', gap: 7 }}>
                <Icon name="spark" size={16} fill="#4a5af0" style={{ color: '#4a5af0' }} />{t('cd.tab.shodan')}
              </div>
              <ProposalAnalysis caseData={c} kind="shodan" />
              {/* 区切り（上：提案 / 下：履歴） */}
              <div style={{ height: 1, background: '#eef0f3', margin: '26px 0 20px' }} />
              {/* 下：商談履歴 */}
              <div style={{ fontSize: 13.5, fontWeight: 700, color: '#1f2430', marginBottom: 14, display: 'flex', alignItems: 'center', gap: 7 }}>
                <Icon name="calendar" size={16} stroke={2} style={{ color: '#4a5af0' }} />{t('tab.meetings')}
              </div>
              <Button variant="default" icon="plus" onClick={() => setOpenMeeting(true)} style={{ marginBottom: 18 }} full>{t('btn.addMeeting')}</Button>
              <div style={{ position: 'relative' }}>
                {mtgs.length > 0 && <div style={{ position: 'absolute', left: 15, top: 8, bottom: 8, width: 2, background: '#eef0f3' }} />}
                {mtgs.map((m) => {
                  const au = D.user(m.authorId);
                  // 予定行の参加リンクは「会議リンク」だけ（案件ページのリンクは出さない）
                  // Google Meet ＞ 会議リンク（担当者用・参加者用は両方表示。本文からの自動判別も含む）
                  // 電話予定・訪問予定にはリンクを一切出さない（時間になったら架電/訪問するだけ。URLが並ぶと紛らわしい）
                  const planLinks = (!m.isPlan || m.method === 'phone' || m.method === 'visit') ? []
                    : casePlanLinkList(c, m.gcal);
                  return (
                    <div key={m.id} style={{ display: 'flex', gap: 14, marginBottom: 18, position: 'relative' }}>
                      <div style={{ width: 32, flex: '0 0 auto', display: 'flex', justifyContent: 'center', zIndex: 1 }}>
                        <div style={{ width: 32, height: 32, borderRadius: '50%', background: m.isPlan ? '#f4f5f7' : '#fff', border: '2px solid #eef0f3', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                          <Icon name={m.isPlan ? (m.method === 'phone' ? 'phone' : m.method === 'visit' ? 'mapPin' : 'calendar') : (methodIcon[m.method] || 'video')} size={15} stroke={2} style={{ color: m.isPlan ? (m.method === 'phone' ? '#ea580c' : '#9aa1ab') : '#4a5af0' }} />
                        </div>
                      </div>
                      <div style={{ flex: 1, minWidth: 0, background: m.isPlan ? '#fbfbfe' : '#fafafb', border: '1px ' + (m.isPlan ? 'dashed #dfe2e8' : 'solid #f0f1f4'), borderRadius: 11, padding: '13px 15px' }}>
                        <div style={{ display: 'flex', alignItems: 'center', gap: 9, marginBottom: 8, flexWrap: 'wrap' }}>
                          <span style={{ fontSize: 13, fontWeight: 700, color: '#1f2430' }}>{fmtDateFull(m.datetime)} {fmtTime(m.datetime) || (m.isPlan ? t('cd.timeTbd') : '')}</span>
                          {m.isPlan
                            ? (m.method === 'phone'
                              ? <span style={{ fontSize: 12, fontWeight: 700, color: '#ea580c', background: '#fdeee2', padding: '1px 8px', borderRadius: 5, display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="phone" size={11} stroke={2.2} />{t('cd.phonePlanned')}</span>
                              : m.method === 'visit'
                                ? <span style={{ fontSize: 12, fontWeight: 700, color: '#2e9e6b', background: '#e6f4ec', padding: '1px 8px', borderRadius: 5, display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="mapPin" size={11} stroke={2.2} />{t('cd.visitPlanned')}</span>
                                : <span style={{ fontSize: 12, fontWeight: 700, color: '#4a5af0', background: '#eef0fe', padding: '1px 8px', borderRadius: 5 }}>{t('cd.meetingPlanned')}</span>)
                            : <span style={{ fontSize: 12, fontWeight: 600, color: '#4a5af0', background: '#eef0fe', padding: '1px 8px', borderRadius: 5 }}>{(D.METHODS[m.method] || {}).label || t('cd.methodOnline')}</span>}
                          {m.source === 'fireflies' && <span style={{ fontSize: 12, fontWeight: 700, color: '#ef5a3c', background: '#ef5a3c18', padding: '2px 7px', borderRadius: 5, display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="spark" size={11} fill="#ef5a3c" />Fireflies AI</span>}
                          {m.source === 'manual' && <span style={{ fontSize: 12, fontWeight: 700, color: '#7b828d', background: '#f0f1f4', padding: '2px 7px', borderRadius: 5 }}>{t('cd.manualRecording')}</span>}
                          {m.gcal && <span style={{ fontSize: 12, fontWeight: 700, color: '#2563eb', background: '#2563eb15', padding: '2px 7px', borderRadius: 5 }}>{t('cd.src.gcal')}</span>}
                          {!m.isPlan && m.probability != null && <span style={{ fontSize: 12, fontWeight: 700, padding: '2px 7px', borderRadius: 5, color: m.probability >= 60 ? '#16a34a' : m.probability >= 35 ? '#d97706' : '#dc2626', background: (m.probability >= 60 ? '#16a34a' : m.probability >= 35 ? '#d97706' : '#dc2626') + '14' }}>{t('cd.winProb', { prob: m.probability })}</span>}
                          <div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 6 }}>
                            {au && <><Avatar user={au} size={20} /><span style={{ fontSize: 12, color: '#7b828d' }}>{au.short}</span></>}
                            {/* 商談記録の削除（管理者/作成者のみ）＝予定登録の誤用で出来た空記録などの掃除 */}
                            {!m.isPlan && !m.isDoc && m.id && (currentUser.role === 'admin' || m.authorId === currentUser.id) && (
                              <IconButton name="x" size={14} title={t('btn.delete')}
                                onClick={async () => { if (!window.confirm(t('common.confirmDelete'))) return; try { await removeMeeting(m.id); } catch (e) { showToast((e && e.message) || 'error', 'x'); } }} />
                            )}
                          </div>
                        </div>
                        {m.isPlan
                          ? <div style={{ fontSize: 12.5, color: '#7b828d' }}>{m.method === 'phone' ? t('cd.upcomingPhone') : m.method === 'visit' ? t('cd.upcomingVisit') : (t('cd.upcomingMeeting') + (planLinks.length ? t('cd.joinFromLinks') : t('cd.autoImportAfterMeeting')))}</div>
                          : <div style={{ fontSize: 13, color: '#3b414b', lineHeight: 1.65 }}>{m.summary}</div>}
                        {/* リンク未登録の商談予定：RC詳細未取込なら「詳細を取込」（RC詳細ページ）、それ以外は手動でURL登録（Bで古い流用リンクを消した予定もここから） */}
                        {m.isPlan && m.method !== 'phone' && m.method !== 'visit' && !planLinks.length && (caseNeedsRcDetailImport(c)
                          ? <a href={c.sourceUrl} target="_blank" rel="noreferrer" style={{ textDecoration: 'none', display: 'inline-block', marginTop: 8 }} title={t('cd.rcDetail.hint')}><Button size="sm" variant="default" icon="download">{t('cd.rcDetail.import')}</Button></a>
                          : <Button size="sm" variant="default" icon="link" style={{ marginTop: 8 }} onClick={() => {
                              const u = window.prompt(t('cd.meetUrl.prompt'));
                              if (u == null || !u.trim()) return;
                              let v = u.trim(); if (!/^https?:\/\//.test(v)) v = 'https://' + v;
                              addMeetingLink(c.id, { label: t('cd.meetUrl.linkLabel'), url: v });
                            }}>{t('cd.meetUrl.register')}</Button>
                        )}
                        {planLinks.length > 0 && (
                          <div style={{ display: 'flex', gap: 8, marginTop: 10, flexWrap: 'wrap' }}>
                            {planLinks.map((l, li) => (
                              <span key={li} style={{ display: 'inline-flex', gap: 4, alignItems: 'stretch' }}>
                                <a href={l.url} target="_blank" rel="noreferrer" style={{ textDecoration: 'none' }}>
                                  <Button size="sm" variant="default" icon="video">{l.label}</Button>
                                </a>
                                {/* リンクをコピー：お客様への案内メール等に貼る用（開かずにURLだけ取れる） */}
                                <button onClick={async () => showToast(...((await copyText(l.url)) ? [t('cd.linkCopied')] : [t('cd.copyFailed'), 'x']))}
                                  title={t('cd.copyLink')}
                                  style={{ display: 'inline-flex', alignItems: 'center', padding: '0 9px', border: '1px solid #e2e5ea', background: '#fff', borderRadius: 8, cursor: 'pointer', color: '#5b626d' }}>
                                  <Icon name="copy" size={13} stroke={2} />
                                </button>
                              </span>
                            ))}
                          </div>
                        )}
                        {m.isDoc && (
                          <button onClick={() => selectTab('meetingDocs')}
                            style={{ display: 'inline-flex', alignItems: 'center', gap: 5, marginTop: 8, border: 'none', background: 'transparent', color: '#4a5af0', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', padding: 0 }}>
                            {t('cd.openMeetingDoc')} <Icon name="arrowRight" size={12} stroke={2} />
                          </button>
                        )}
                        {m.nextAction && (
                          <div style={{ display: 'flex', alignItems: 'flex-start', gap: 7, marginTop: 10, padding: '7px 10px', background: '#fff', border: '1px solid #eceef1', borderRadius: 8 }}>
                            <Icon name="arrowRight" size={13} stroke={2} style={{ color: '#ea580c', flex: '0 0 auto', marginTop: 2 }} />
                            <span style={{ fontSize: 12, color: '#7b828d', fontWeight: 600, whiteSpace: 'nowrap', flex: '0 0 auto', marginTop: 1 }}>{t('cd.nextAction')}</span>
                            <span style={{ fontSize: 12.5, color: '#2b2f38' }}>{m.nextAction}</span>
                          </div>
                        )}
                        {!m.isPlan && (
                          <FeedbackRow value={m.customerFeedback}
                            onSave={(text) => {
                              if (m.isDoc) patchCase(c.id, { meetingDocs: (c.meetingDocs || []).map(d => d.id === m.docId ? { ...d, customerFeedback: text } : d) });
                              else updateMeeting(m.id, { customerFeedback: text });
                            }} />
                        )}
                        {m.calendar && <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginTop: 8, fontSize: 12, color: '#16a34a' }}><Icon name="check2" size={12} stroke={2.2} />{t('cd.calendarRegistered')}</div>}
                      </div>
                    </div>
                  );
                })}
                {mtgs.length === 0 && <div style={{ padding: '30px 0', textAlign: 'center', color: '#9aa1ab', fontSize: 13 }}>{t('cd.noMeetingRecords')}</div>}
              </div>
            </div>
          )}

          {tab === 'content' && (
            <div style={{ padding: 22 }}>
              {/* 発注者の回答（リード=発注者の回答。商談ではなく案件内容の脈絡なのでこのタブへ移設） */}
              <OrdererReview caseData={c} />
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
                <span style={{ fontSize: 12.5, fontWeight: 700, color: '#1c1f26' }}>{t('cd.originalText')}</span>
                {c.source === 'scrape' && <span style={{ fontSize: 12, color: '#9aa1ab', display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="link" size={12} stroke={2} />{t('case-form.lock')}</span>}
              </div>
              {/* 本文重複の警告（取込ミスで別案件の相談原文が誤爆した痕跡＝同一本文が複数案件）。目に見える安全網 */}
              {(bodyDupMap[c.id] || []).length > 0 && (
                <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', background: '#fef3f2', border: '1px solid #fbd5cf', borderRadius: 10, padding: '11px 14px', marginBottom: 12 }}>
                  <Icon name="alert" size={16} stroke={2} style={{ color: '#c0392b', flex: '0 0 auto', marginTop: 1 }} />
                  <div style={{ fontSize: 12.5, color: '#7a2e26', lineHeight: 1.6 }}>
                    <b>{t('cd.bodyDup.title', { n: (bodyDupMap[c.id] || []).length })}</b>
                    <div style={{ marginTop: 3, color: '#96463d' }}>{t('cd.bodyDup.desc')}</div>
                    <div style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                      {(bodyDupMap[c.id] || []).slice(0, 8).map(oid => { const oc = D.caseById(oid); return (
                        <button key={oid} onClick={() => navigate('case', oid)} style={{ fontSize: 11.5, color: '#4a5af0', background: '#fff', border: '1px solid #e4d6d3', borderRadius: 6, padding: '2px 8px', cursor: 'pointer', fontFamily: 'inherit' }}>{oc ? (oc.title || oid).slice(0, 22) : oid}</button>
                      ); })}
                    </div>
                  </div>
                </div>
              )}
              <div style={{ fontSize: 13.5, color: '#3b414b', lineHeight: 1.85, background: '#fafafb', border: '1px solid #f0f1f4', borderRadius: 10, padding: '16px 18px', whiteSpace: 'pre-wrap' }}>{c.body}</div>
              <div style={{ fontSize: 12.5, fontWeight: 700, color: '#1c1f26', margin: '20px 0 10px' }}>{t('cd.internalNotes')}</div>
              <NoteEditor caseData={c} editable={canEdit} onSave={(v) => saveCase({ id: c.id, note: v })} />
            </div>
          )}

          {tab === 'proposal' && <DocTab caseData={c} category="提案書" statusField="proposalStatus" ownerField="proposalOwnerId" dueField="due" title={t('cd.tab.proposal')} linksField="proposalLinks" analysis="proposal" />}
          {tab === 'quote' && <DocTab caseData={c} category="見積書" statusField="quoteStatus" ownerField="quoteOwnerId" dueField="quoteDue" title={t('cd.tab.quote')} linksField="quoteLinks" analysis="quote" />}
          {tab === 'prototype' && <DocTab caseData={c} category="プロトタイプ" statusField="prototypeStatus" ownerField="prototypeOwnerId" dueField="prototypeDue" title={t('cd.tab.prototype')} linksField="prototypeLinks" analysis="prototype" analysis2="protoFeedback" editable={can('prototypeEdit')} canAdd={can('prototypeAdd')} />}
          {tab === 'attach' && <AttachTab caseData={c} />}
          {tab === 'meetingDocs' && <MeetingDocsTab caseData={c} />}
          {tab === 'mail' && <MailTab caseData={c} />}

          {tab === 'logs' && (
            <div style={{ padding: 20 }}>
              {logs.map((l, i) => {
                const u = l.userId ? D.user(l.userId) : null;
                return (
                  <div key={l.id} style={{ display: 'flex', gap: 12, padding: '10px 0', borderBottom: i === logs.length - 1 ? 'none' : '1px solid #f4f5f7', alignItems: 'flex-start' }}>
                    {u ? <Avatar user={u} size={26} /> : <div style={{ width: 26, height: 26, borderRadius: '50%', background: '#eef0fe', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: '0 0 auto' }}><Icon name="refresh" size={13} stroke={2} style={{ color: '#4a5af0' }} /></div>}
                    <div style={{ flex: 1 }}>
                      <div style={{ fontSize: 13, color: '#2b2f38' }}><span style={{ fontWeight: 600 }}>{u ? u.name : t('cd.system')}</span> {l.text}</div>
                      <div style={{ fontSize: 12, color: '#4a5af0', marginTop: 2 }}>{fmtDateTime(l.at)}</div>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </Card>
      </div>
      {openMeeting && <MeetingModal caseId={c.id} onClose={() => setOpenMeeting(false)} />}
      {openEdit && <CaseFormModal editCase={c} onClose={() => setOpenEdit(false)} />}
      {achOpen && <AchievementDraftModal caseData={c} onClose={() => setAchOpen(false)} />}
    </Page>
  );
}

Object.assign(window, { CaseDetail, StatusMenu, NextActionsCard });
