/* ============================================================
   案件詳細 — 見積表（QuoteSheet）。case-detail.jsx から分離（結合低減）。
   依存はすべてグローバル（ui.jsx/store.jsx/api.js/i18n）＝render時解決。
   ============================================================ */
const QS_TAX_OPTS = [10, 8, 0];
const qsNum = (v, d = 0) => { const n = parseFloat(String(v == null ? '' : v).replace(/[^\d.-]/g, '')); return isFinite(n) ? n : d; };
const qsYen = (n) => '¥' + Math.round(n).toLocaleString('ja-JP');
const qsEmptyItem = () => ({ type: 'item', name: '', qty: 1, unit: '式', unitPrice: '', taxRate: 10 });
/* 実物見積書の機能行は先頭に「- 」が付く（▼見出しの下に「- ログイン／登録機能」等）。
   挿入時に付け、機能マスタへ登録する時は外す（マスタは素の機能名で持つ） */
const qsDashName = (name) => { const s = String(name || '').trim(); return (!s || /^[-▼・]/.test(s)) ? s : '- ' + s; };
const qsStripDash = (name) => String(name || '').replace(/^-\s*/, '').trim();
const qsEmptyText = () => ({ type: 'text', name: '' });
const qsToday = () => { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; };
const qsAddDays = (n) => { const d = new Date(); d.setDate(d.getDate() + n); const p = (x) => String(x).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; };
const qsNowStamp = () => { const d = new Date(); const p = (x) => String(x).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; };
function QuoteSheet({ caseData, ctrlRef }) {   // ctrlRef: 親（スタジオ）が flush（未保存の手入力を即サーバ保存）を呼ぶための窓口
  const { showToast, patchCase, currentUser, addLog, can, mergeFeatureMaster, cases } = useStore();
  const D = window.APP_DATA;
  const cust = (D.customers || []).find(c => c.id === caseData.customerId) || null;
  const [sheet, setSheet] = React.useState(() => caseData.quoteSheet || {
    partnerName: (cust && cust.company) || '', partnerTitle: '御中', contactName: (cust && cust.contact) || '',
    partnerAddress1: (cust && cust.address) || '', partnerDept: (cust && cust.contactDept) || '',   // 取引先詳細は顧客台帳から前充填（freeeの取引先情報に入る）
    subject: caseData.title || '', quotationDate: qsToday(), expiryDate: qsAddDays(30), items: [],
  });
  const [freee, setFreee] = React.useState(window.__FREEE_STATUS || null);
  const [aiBusy, setAiBusy] = React.useState(false);
  const [fileBusy, setFileBusy] = React.useState(false);
  const [pickOpen, setPickOpen] = React.useState(false);
  const [fmOpen, setFmOpen] = React.useState(false); // 機能マスタから追加 ピッカー
  const [fmQ, setFmQ] = React.useState('');
  const [sendBusy, setSendBusy] = React.useState(false);
  const [saved, setSaved] = React.useState(true);
  const firstRef = React.useRef(true);
  const sheetRef = React.useRef(sheet);
  sheetRef.current = sheet;
  /* スタジオのチャット送信前に呼ばれる flush：入力途中（800msデバウンス待ち）の内容を
     即サーバ保存し、AIが古い表を読んで手入力を巻き戻す事故を防ぐ */
  React.useEffect(() => {
    if (!ctrlRef) return;
    ctrlRef.current = { flush: async () => { const st = qsStamp(sheetRef.current); await API.patchAwait('cases', caseData.id, { quoteSheet: st }); patchCase(caseData.id, { quoteSheet: st }); setSaved(true); } };
    return () => { ctrlRef.current = null; };
  }, [caseData.id]);

  React.useEffect(() => {
    if (window.__FREEE_STATUS) { setFreee(window.__FREEE_STATUS); return; }
    API.freeeStatus().then(s => { window.__FREEE_STATUS = s; setFreee(s); }).catch(() => {});
  }, []);
  /* 作成者・最終編集者スタンプ：保存する瞬間に付与（ローカルstateには入れない＝保存ループ防止）。
     表示は caseData.quoteSheet 側から読む */
  const qsMe = (currentUser && (currentUser.short || currentUser.name)) || '';
  const qsStamp = (s) => ({ ...s, createdBy: s.createdBy || qsMe, createdAt: s.createdAt || qsNowStamp(), updatedBy: qsMe, updatedAt: qsNowStamp() });
  React.useEffect(() => {
    if (firstRef.current) { firstRef.current = false; return; }
    const t = setTimeout(() => { patchCase(caseData.id, { quoteSheet: qsStamp(sheet) }); setSaved(true); }, 800);
    return () => clearTimeout(t);
  }, [sheet]);

  const setField = (k, v) => { setSaved(false); setSheet(s => ({ ...s, [k]: v })); };
  const setItem = (i, k, v) => { setSaved(false); setSheet(s => ({ ...s, items: s.items.map((it, idx) => idx === i ? { ...it, [k]: v } : it) })); };
  const addItem = (type) => { setSaved(false); setSheet(s => ({ ...s, items: [...s.items, type === 'text' ? qsEmptyText() : qsEmptyItem()] })); };
  const removeItem = (i) => { setSaved(false); setSheet(s => ({ ...s, items: s.items.filter((_, idx) => idx !== i) })); };
  const moveItem = (i, dir) => { const j = i + dir; if (j < 0 || j >= sheet.items.length) return; setSaved(false); setSheet(s => { const arr = s.items.slice(); const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; return { ...s, items: arr }; }); };
  const isAdmin = can('freeeSend'); // freee送信の可否（権限設定で制御。表の編集・保存は全員可）
  const saveNow = () => { patchCase(caseData.id, { quoteSheet: qsStamp(sheet) }); setSaved(true); showToast(t('cd.toast.quoteSheetSaved')); };
  /* 別名保存＝1案件に複数の見積書を持てる（case.quoteVersions に名前つきスナップショット・最大20件）。
     読み込むと現在の表がその版の内容に置き換わる */
  const [verOpen, setVerOpen] = React.useState(false);
  const versions = Array.isArray(caseData.quoteVersions) ? caseData.quoteVersions : [];
  const saveAs = () => {
    const def = (sheet.subject || caseData.title || '見積書') + '（' + qsToday() + '）';
    const name = (window.prompt(t('cd.qs.saveAsAsk'), def) || '').trim();
    if (!name) return;
    const v = { id: 'qv' + Date.now(), name: name.slice(0, 60), at: qsToday(), by: qsMe, sheet: JSON.parse(JSON.stringify(qsStamp(sheet))) };
    patchCase(caseData.id, { quoteVersions: [v, ...versions].slice(0, 20), quoteSheet: qsStamp(sheet) });
    setSaved(true); showToast(t('cd.qs.saveAsDone'));
  };
  const loadVersion = (v) => {
    if (!window.confirm(t('cd.qs.loadVerConfirm', { name: v.name }))) return;
    const s = JSON.parse(JSON.stringify(v.sheet || {}));
    setSheet(s); setVerOpen(false);
    patchCase(caseData.id, { quoteSheet: qsStamp(s) });
    setSaved(true); showToast(t('cd.qs.loadVerDone'));
  };
  const removeVersion = (v) => {
    if (!window.confirm(t('cd.qs.delVerConfirm', { name: v.name }))) return;
    patchCase(caseData.id, { quoteVersions: versions.filter(x => x.id !== v.id) });
  };
  // この見積書の明細を機能マスタ（料金カタログ）に登録＝案件で作った見積書を会社の値付け資産に貯める。出典＝件名
  const registerToMaster = () => {
    const items = (sheet.items || []).filter(it => it.type !== 'text' && String(it.name || '').trim() && Number(it.unitPrice) > 0)
      .map(it => ({ name: qsStripDash(it.name), unit: it.unit || '式', unitPrice: Math.round(Number(it.unitPrice) || 0),   // マスタは素の機能名（「- 」なし）で持つ
        taxRate: [10, 8, 0].includes(Number(it.taxRate)) ? Number(it.taxRate) : 10, source: String(sheet.subject || '').trim() || caseData.title || '案件見積書' }));
    if (!items.length) { showToast(window.t("label.extra28"), 'x'); return; }
    const r = mergeFeatureMaster(items);
    showToast('機能マスタに ' + r.added + '件追加・' + r.updated + '件更新しました（ナレッジ→見積もり用で確認できます）');
  };

  // 単価の参照元＝機能マスタ＋全案件の過去見積書の明細（会社の値付け履歴すべて）。マスタが空でも過去見積から拾える
  const priceRefs = () => {
    const refs = [];
    (D.featureMaster || []).forEach(f => { if (f.unitPrice != null) refs.push({ name: f.name, unitPrice: Math.round(Number(f.unitPrice) || 0), unit: f.unit || '式', taxRate: f.taxRate }); });
    (cases || []).forEach(c => { const its = (c.quoteSheet && c.quoteSheet.items) || []; its.forEach(it => { if (it.type !== 'text' && Number(it.unitPrice) > 0) refs.push({ name: it.name, unitPrice: Math.round(Number(it.unitPrice)), unit: it.unit || '式', taxRate: it.taxRate }); }); });
    return refs;
  };
  // AIが出した機能名を参照元に名寄せ（正規化＋部分一致）して単価を採用
  const fmMatch = (name, refs) => {
    const norm = (s) => String(s || '').toLowerCase().replace(/[\s（）()・,、。\/｜|]/g, '');
    const n = norm(name); if (n.length < 2) return null;
    let m = refs.find(f => norm(f.name) === n);
    if (!m) m = refs.find(f => { const fn = norm(f.name); return fn.length >= 3 && (fn.includes(n) || n.includes(fn)); });
    return (m && m.unitPrice != null) ? m : null;
  };
  const aiFill = async () => {
    if (aiBusy) return;
    let feats = (caseData.quoteAnalysis && caseData.quoteAnalysis.features) || [];
    if (!feats.length) {
      if (!window.confirm(t('cd.confirm.generateAnalysisNow'))) return;
      setAiBusy(true);
      try { const r = await API.quotePrep(caseData.id); patchCase(caseData.id, { quoteAnalysis: r.analysis }); feats = (r.analysis && r.analysis.features) || []; }
      catch (e) { showToast(e.message || t('cd.toast.analysisFailed'), 'x'); setAiBusy(false); return; }
      setAiBusy(false);
    }
    if (!feats.length) { showToast(t('cd.toast.noFeaturesToImport'), 'x'); return; }
    if (sheet.items.length && !window.confirm(t('cd.confirm.replaceItems'))) return;
    // ① まず参照元（機能マスタ＋過去見積書）の単価を名寄せで自動セット
    const refs = priceRefs();
    let priced = 0;
    const items = feats.map(f => {
      const name = String(f.name || '') + (f.plan ? `（${f.plan}）` : '');
      const tplUnit = String(f.unit || '').trim(); // quotePrep が「お手本の見積書」に合わせて付けた単位（式/人月 等）
      const m = fmMatch(f.name || name, refs);
      if (m) { priced++; return { type: 'item', name: qsDashName(name), qty: 1, unit: m.unit || tplUnit || '式', unitPrice: String(m.unitPrice), taxRate: [10, 8, 0].includes(Number(m.taxRate)) ? Number(m.taxRate) : 10 }; }
      return { type: 'item', name: qsDashName(name), qty: 1, unit: tplUnit, unitPrice: '', taxRate: 10 };
    });
    // ② 名寄せで付かなかった機能（＝履歴に無い新規機能）は、AIに相場で単価を見積もらせる。サーバ未対応/失敗時は空のまま手入力
    let aiEstimated = 0;
    const unpriced = items.filter(it => !String(it.unitPrice));
    if (unpriced.length) {
      setAiBusy(true);
      try {
        const catalog = refs.map(r => ({ name: r.name, unitPrice: r.unitPrice, unit: r.unit }));
        const pr = await API.quotePrice(unpriced.map(it => it.name), catalog);
        const nrm = (s) => String(s || '').toLowerCase().replace(/[\s（）()・,、。\/｜|]/g, '');
        const pmap = {}; ((pr && pr.prices) || []).forEach(p => { pmap[nrm(p.name)] = p; });
        items.forEach(it => { if (!String(it.unitPrice)) { const p = pmap[nrm(it.name)]; if (p && p.unitPrice > 0) { it.unitPrice = String(p.unitPrice); if (!String(it.unit).trim()) it.unit = (String(p.unit) === '個' ? '式' : (p.unit || '式')); it.taxRate = [10, 8, 0].includes(Number(p.taxRate)) ? Number(p.taxRate) : it.taxRate; aiEstimated++; } } });
      } catch (_) { /* サーバ未対応（railway未反映）/失敗時は単価空のまま */ }
      setAiBusy(false);
    }
    items.forEach(it => { if (it.type === 'item' && !String(it.unit).trim()) it.unit = '式'; }); // 単位が空なら既定'式'（お手本/quote-priceで付かなかった分）
    setSaved(false);
    setSheet(s => ({
      ...s,
      partnerName: s.partnerName || (cust && cust.company) || '',
      contactName: s.contactName || (cust && cust.contact) || '',
      subject: s.subject || caseData.title || '',
      items,
    }));
    const setCount = priced + aiEstimated;
    showToast('AIが' + feats.length + '件を提案' + (setCount ? '／' + setCount + '件に単価をセット' + (aiEstimated ? '（AI見積もり' + aiEstimated + '件含む）' : '') : '（単価は手入力）') + 'しました');
  };

  // 見積書まるごと生成：案件内容＋機能マスタ＋過去見積のお手本から、フェーズ構造＋明細（数量・単価）を一括生成
  const quoteDraft = async () => {
    if (aiBusy) return;
    if (sheet.items.length && !window.confirm(t('cd.confirm.replaceItems'))) return;
    setAiBusy(true);
    try {
      const r = await API.quoteDraft(caseData.id);
      const items = (r.items || []).map(it => String(it.type) === 'text'
        ? { type: 'text', name: String(it.name || '') }
        : { type: 'item', name: String(it.name || ''), qty: it.qty != null ? it.qty : 1, unit: it.unit || '式', unitPrice: (it.unitPrice == null || it.unitPrice === 0 || it.unitPrice === '') ? '' : String(it.unitPrice), taxRate: [10, 8, 0].includes(Number(it.taxRate)) ? Number(it.taxRate) : 10 });
      if (!items.filter(it => it.type !== 'text').length) { showToast(t('cd.qs.draftEmpty'), 'x'); setAiBusy(false); return; }
      setSaved(false);
      setSheet(s => ({ ...s, partnerName: s.partnerName || (cust && cust.company) || '', contactName: s.contactName || (cust && cust.contact) || '', subject: s.subject || caseData.title || '', note: (r.note != null && r.note !== '') ? r.note : s.note, items }));
      const st = r.stats || {};
      showToast(t('cd.qs.draftDone', { n: st.total || items.filter(it => it.type !== 'text').length, p: st.priced || 0 })
        + (st.protoAdded ? t('cd.qs.draftProtoAdded', { n: st.protoAdded }) : ''));
    } catch (e) { showToast(e.message || t('cd.toast.analysisFailed'), 'x'); }
    setAiBusy(false);
  };

  // 取込候補（この案件の「見積書」カテゴリの PDF/画像）。新しい順
  const quoteFileList = (caseData.attachments || [])
    .filter(a => a.category === '見積書' && (String(a.mime || '').toLowerCase() === 'application/pdf' || /\.pdf$/i.test(a.name || '') || String(a.mime || '').indexOf('image/') === 0 || /\.(png|jpe?g|gif|webp)$/i.test(a.name || '')))
    .slice().sort((a, b) => String(b.at || '').localeCompare(String(a.at || '')));

  // 指定ファイルを AI で読み取り、明細を表に取り込む
  const runImport = async (att) => {
    setPickOpen(false);
    if (fileBusy || !att) return;
    if (sheet.items.length && !window.confirm(t('cd.confirm.replaceItems'))) return;
    setFileBusy(true);
    try {
      const r = await API.quoteFromAttachment(caseData.id, att.id);
      const items = (r.items || []).map(it => String(it.type) === 'text'
        ? { type: 'text', name: String(it.name || '') }
        : { type: 'item', name: qsDashName(it.name), qty: it.qty != null ? it.qty : 1, unit: it.unit || '式', unitPrice: it.unitPrice == null ? '' : String(it.unitPrice), taxRate: [10, 8, 0].includes(Number(it.taxRate)) ? Number(it.taxRate) : 10 }
      ).filter(it => String(it.name || '').trim());
      if (!items.length) { showToast(t('cd.toast.noFeaturesToImport'), 'x'); setFileBusy(false); return; }
      setSaved(false);
      setSheet(s => ({ ...s, partnerName: s.partnerName || r.partnerName || '', subject: s.subject || r.subject || '', note: (r.note != null && r.note !== '') ? r.note : s.note, items }));
      showToast(t('cd.toast.fileImported', { n: items.length }));
    } catch (e) { showToast(e.message || t('cd.toast.analysisFailed'), 'x'); }
    setFileBusy(false);
  };
  // ボタン押下：0件→案内、1件→即取込、複数→ファイル選択メニュー
  const fileFill = () => {
    if (fileBusy) return;
    if (!quoteFileList.length) { showToast(t('cd.toast.noQuoteFile'), 'x'); return; }
    if (quoteFileList.length === 1) { runImport(quoteFileList[0]); return; }
    setPickOpen(o => !o);
  };

  // 機能マスタ（料金カタログ）から機能＋標準単価を1行ずつ追加（置換ではなく追記。ポップアップは開いたまま＝連続追加）
  const fmRows = (D.featureMaster || []).slice()
    .filter(f => !fmQ || (f.name || '').includes(fmQ) || (f.category || '').includes(fmQ))
    .sort((a, b) => (b.count || 0) - (a.count || 0) || String(a.name).localeCompare(String(b.name)));
  const addFromMaster = (f) => {
    setSaved(false);
    setSheet(s => ({ ...s, items: [...s.items, { type: 'item', name: qsDashName(f.name), qty: 1, unit: f.unit || '式', unitPrice: f.unitPrice != null ? String(f.unitPrice) : '', taxRate: [10, 8, 0].includes(Number(f.taxRate)) ? Number(f.taxRate) : 10 }] }));
    showToast('「' + f.name + '」を追加しました');
  };

  const byRate = {};
  let subtotal = 0;
  sheet.items.forEach(it => { if (it.type === 'text') return; const amt = qsNum(it.qty) * qsNum(it.unitPrice); const r = qsNum(it.taxRate); subtotal += amt; if (!byRate[r]) byRate[r] = { base: 0, tax: 0 }; byRate[r].base += amt; byRate[r].tax += amt * (r / 100); });
  const taxRates = Object.keys(byRate).map(Number).sort((a, b) => b - a);
  const taxTotal = taxRates.reduce((s, r) => s + byRate[r].tax, 0);
  const total = subtotal + taxTotal;
  const q = caseData.freeeQuotation || null;
  const connected = !!(freee && freee.connected);

  const sendFreee = async () => {
    if (sendBusy) return;
    const named = sheet.items.filter(it => String(it.name || '').trim());
    if (!named.length) { showToast(t('cd.toast.enterAtLeastOneItem'), 'x'); return; }
    if (!window.confirm(t(q && q.id ? 'cd.confirm.updateFreeeQuotation' : 'cd.confirm.createFreeeQuotation', { count: named.length, total: qsYen(total) }))) return;
    setSendBusy(true);
    try {
      patchCase(caseData.id, { quoteSheet: sheet });
      const r = await API.freeeCreateQuotation(caseData.id, sheet);   // 作成済みならサーバが自動で「更新」（同じfreee見積書を最新化）
      patchCase(caseData.id, { freeeQuotation: r.quotation });
      if (addLog) addLog(caseData.id, 'freee', t('cd.log.freeeQuotationCreated'));
      showToast(t(r.updated ? 'cd.toast.freeeQuotationUpdated' : 'cd.toast.freeeQuotationCreated') + (r.quotation && r.quotation.number ? t('cd.freeeQuotationNo', { number: r.quotation.number }) : ''));
    } catch (e) { showToast(e.message || t('cd.toast.freeeQuotationFailed'), 'x'); }
    setSendBusy(false);
  };

  const cellInput = { width: '100%', border: '1px solid #e2e5ea', borderRadius: 7, padding: '6px 8px', fontSize: 13, fontFamily: 'inherit', color: '#2b2f38', background: '#fff' };
  const hdrInput = { ...cellInput, padding: '7px 10px' };
  const th = { fontSize: 11.5, fontWeight: 700, color: '#9aa1ab', textAlign: 'left', padding: '0 8px 6px' };
  const qsIconBtn = (disabled) => ({ border: 'none', background: 'transparent', cursor: disabled ? 'default' : 'pointer', color: disabled ? '#e6e8ec' : '#b4bac3', padding: 3, lineHeight: 0 });
  const rowActions = (i, isLast) => (
    <td style={{ padding: '3px 2px', whiteSpace: 'nowrap', textAlign: 'right' }}>
      <button onClick={() => moveItem(i, -1)} disabled={i === 0} title={t('cd.qs.moveUp')} style={qsIconBtn(i === 0)}><Icon name="chevronDown" size={13} stroke={2.2} style={{ transform: 'rotate(180deg)' }} /></button>
      <button onClick={() => moveItem(i, 1)} disabled={isLast} title={t('cd.qs.moveDown')} style={qsIconBtn(isLast)}><Icon name="chevronDown" size={13} stroke={2.2} /></button>
      <button onClick={() => removeItem(i)} title={t('btn.delete')} style={qsIconBtn(false)}><Icon name="x" size={13} stroke={2.2} /></button>
    </td>
  );

  return (
    <div style={{ marginBottom: 24, border: '1px solid #e6e8ec', borderRadius: 12, overflow: 'hidden' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '11px 16px', background: '#fafbfc', borderBottom: '1px solid #eef0f3', flexWrap: 'wrap' }}>
        <Icon name="cases" size={15} stroke={2} style={{ color: '#4a5af0' }} />
        <span style={{ fontSize: 13, fontWeight: 700, color: '#1c1f26' }}>{t('cd.quoteSheetTitle')}</span>
        <span style={{ fontSize: 11.5, color: saved ? '#16a34a' : '#b4bac3' }}>{saved ? t('cd.savedMark') : t('cd.unsaved')}</span>
        {(() => { const m = caseData.quoteSheet || {};   // 作成・最終編集スタンプ（保存時に付与された値を表示）
          return (m.createdBy || m.updatedBy) ? (
            <span style={{ fontSize: 11, color: '#9aa1ab' }}>
              {m.createdBy ? `${t('cd.qs.metaCreated')}：${m.createdBy}・${m.createdAt || ''}` : ''}
              {m.updatedBy ? `　${t('cd.qs.metaUpdated')}：${m.updatedBy}・${m.updatedAt || ''}` : ''}
            </span>
          ) : null; })()}
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <div style={{ position: 'relative' }}>
            <Button size="sm" variant="default" icon={fileBusy ? 'refresh' : 'attach'} onClick={fileFill} disabled={fileBusy} title={t('cd.qs.importFromFileHint')}>{fileBusy ? t('cd.qs.fileImporting') : (t('cd.qs.importFromFile') + (quoteFileList.length > 1 ? ` (${quoteFileList.length})` : ''))}</Button>
            {pickOpen && <div onClick={() => setPickOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 30 }} />}
            {pickOpen && (
              <div style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, zIndex: 31, background: '#fff', border: '1px solid #e2e5ea', borderRadius: 10, boxShadow: '0 10px 30px rgba(20,24,40,0.14)', width: 340, maxWidth: '82vw', padding: 6 }}>
                <div style={{ fontSize: 11, color: '#9aa1ab', padding: '4px 8px 6px', fontWeight: 600 }}>{t('cd.qs.pickFile')}</div>
                {quoteFileList.map(att => (
                  <div key={att.id} className="row-hover" onClick={() => runImport(att)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '8px 9px', borderRadius: 8, cursor: 'pointer' }}>
                    <Icon name="attach" size={14} stroke={2} style={{ color: '#16a34a', flex: '0 0 auto' }} />
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div style={{ fontSize: 12.5, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{att.name}</div>
                      <div style={{ fontSize: 11, color: '#9aa1ab' }}>{fmtSize(att.size)} · {fmtDate(att.at, true)} {fmtTime(att.at)}</div>
                    </div>
                  </div>
                ))}
              </div>
            )}
          </div>
          <div style={{ position: 'relative' }}>
            <Button size="sm" variant="default" icon="chart" onClick={() => setFmOpen(o => !o)} disabled={fileBusy} title={window.t("extra.quote.addFromMasterHint")}>{t('cd.qs.fromMaster')}{(D.featureMaster || []).length ? ` (${(D.featureMaster || []).length})` : ''}</Button>
            {can('manageKnowledge') && <Button size="sm" variant="default" icon="plus" onClick={registerToMaster} disabled={fileBusy} title={window.t("extra.quote.saveMasterHint")}>{window.t("extra.quote.saveMaster")}</Button>}
            {fmOpen && <div onClick={() => setFmOpen(false)} style={{ position: 'fixed', inset: 0, zIndex: 30 }} />}
            {fmOpen && (
              <div style={{ position: 'absolute', top: '100%', right: 0, marginTop: 6, zIndex: 31, background: '#fff', border: '1px solid #e2e5ea', borderRadius: 10, boxShadow: '0 10px 30px rgba(20,24,40,0.14)', width: 380, maxWidth: '86vw', padding: 6 }}>
                {(D.featureMaster || []).length === 0 ? (
                  <div style={{ fontSize: 12, color: '#9aa1ab', padding: '14px 12px', lineHeight: 1.7 }}>{t('cd.qs.masterEmpty')}</div>
                ) : (
                  <React.Fragment>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 6px 8px', borderBottom: '1px solid #f0f1f4', marginBottom: 4 }}>
                      <Icon name="search" size={13} stroke={2} style={{ color: '#9aa1ab' }} />
                      <input autoFocus value={fmQ} onChange={e => setFmQ(e.target.value)} placeholder={t('cd.qs.masterSearch')} style={{ border: 'none', outline: 'none', fontSize: 12.5, width: '100%', fontFamily: 'inherit', background: 'transparent' }} />
                    </div>
                    <div style={{ maxHeight: 320, overflowY: 'auto' }}>
                      {fmRows.map(f => (
                        <div key={f.id} className="row-hover" onClick={() => addFromMaster(f)} style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '7px 9px', borderRadius: 8, cursor: 'pointer' }}>
                          <div style={{ minWidth: 0, flex: 1 }}>
                            <div style={{ fontSize: 12.5, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{f.name}</div>
                            <div style={{ fontSize: 11, color: '#9aa1ab' }}>{(f.category || '—') + ' · ' + (f.count || 1) + window.t("extra.unit.times")}</div>
                          </div>
                          <div style={{ fontSize: 12.5, fontWeight: 700, color: '#4a5af0', fontFamily: 'var(--mono)', whiteSpace: 'nowrap' }}>{qsYen(f.unitPrice)}<span style={{ color: '#9aa1ab', fontWeight: 500 }}>/{f.unit || window.t("extra.unit.set")}</span></div>
                          <Icon name="plus" size={14} stroke={2.2} style={{ color: '#16a34a', flex: '0 0 auto' }} />
                        </div>
                      ))}
                      {fmRows.length === 0 && <div style={{ fontSize: 12, color: '#9aa1ab', padding: '12px 10px', textAlign: 'center' }}>{t('cd.qs.masterNoHit')}</div>}
                    </div>
                  </React.Fragment>
                )}
              </div>
            )}
          </div>
          <Button size="sm" variant="default" icon={aiBusy ? 'refresh' : 'spark'} onClick={aiFill} disabled={aiBusy} title={window.t("extra.quote.suggestHint")}>{aiBusy ? t('cd.aiGenerating') : t('cd.aiImport')}</Button>
          <Button size="sm" variant="default" icon={aiBusy ? 'refresh' : 'spark'} onClick={quoteDraft} disabled={aiBusy} title={window.t("extra.quote.draftHint")}>{aiBusy ? t('cd.aiGenerating') : t('cd.qs.draftBtn')}</Button>
          <Button size="sm" variant="primary" icon="check" onClick={saveNow} disabled={saved}>{t('btn.save')}</Button>
          {/* ドロップダウンだとスタジオの枠（overflow/重なり順）でクリップされ押せない事故があったため
              Modal に変更（2026-08-18・全画面で安定） */}
          <Button size="sm" variant="default" icon="copy" onClick={() => setVerOpen(true)} title={t('cd.qs.saveAsHint')}>
            {t('cd.qs.versions')}{versions.length ? ` (${versions.length})` : ''}
          </Button>
          {verOpen && (
            <Modal open onClose={() => setVerOpen(false)} title={t('cd.qs.versions')} width={460} subtitle={t('cd.qs.saveAsHint')}>
              <button onClick={() => { setVerOpen(false); saveAs(); }}
                style={{ display: 'block', width: '100%', textAlign: 'left', fontSize: 13, fontWeight: 700, padding: '10px 12px', borderRadius: 9, border: 'none', background: '#eef0fd', color: '#4a5af0', cursor: 'pointer', fontFamily: 'inherit' }}>
                ＋ {t('cd.qs.saveAs')}
              </button>
              {versions.length === 0 && <div style={{ fontSize: 12, color: '#9aa1ab', padding: '12px 4px 4px', lineHeight: 1.7 }}>{t('cd.qs.verEmpty')}</div>}
              <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column' }}>
                {versions.map(v => (
                  <div key={v.id} className="row-hover" style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '9px 10px', borderRadius: 9 }}>
                    <div onClick={() => loadVersion(v)} style={{ minWidth: 0, flex: 1, cursor: 'pointer' }}>
                      <div style={{ fontSize: 13, fontWeight: 600, color: '#2b2f38', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{v.name}</div>
                      <div style={{ fontSize: 11.5, color: '#9aa1ab', marginTop: 1 }}>{v.at}{v.by ? '・' + v.by : ''}・{((v.sheet || {}).items || []).filter(x => x.type !== 'text').length}{t('cd.qs.verRows')}</div>
                    </div>
                    <Button size="sm" variant="default" onClick={() => loadVersion(v)}>{t('cd.qs.verOpen')}</Button>
                    <button onClick={() => removeVersion(v)} title={t('btn.delete')}
                      style={{ flex: '0 0 auto', border: 'none', background: 'none', color: '#b6bcc6', cursor: 'pointer', fontSize: 15, fontFamily: 'inherit', padding: 4 }}>✕</button>
                  </div>
                ))}
              </div>
            </Modal>
          )}
        </div>
      </div>
      <div style={{ padding: 16 }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10, marginBottom: 16 }}>
          <div><div style={th}>{t('cd.qs.partnerName')}</div><input style={hdrInput} value={sheet.partnerName} onChange={e => setField('partnerName', e.target.value)} placeholder={t('cd.qs.partnerNamePlaceholder')} /></div>
          <div><div style={th}>{t('cd.qs.contactName')}</div><input style={hdrInput} value={sheet.contactName} onChange={e => setField('contactName', e.target.value)} placeholder={t('cd.qs.contactNamePlaceholder')} /></div>
          <div><div style={th}>{t('cd.qs.subject')}</div><input style={hdrInput} value={sheet.subject} onChange={e => setField('subject', e.target.value)} placeholder={t('cd.qs.subjectPlaceholder')} /></div>
          <div><div style={th}>{t('cd.qs.quotationDate')}</div><input type="date" style={hdrInput} value={sheet.quotationDate} onChange={e => setField('quotationDate', e.target.value)} /></div>
          <div><div style={th}>{t('cd.qs.expiryDate')}</div><input type="date" style={hdrInput} value={sheet.expiryDate || ''} onChange={e => setField('expiryDate', e.target.value)} /></div>
          {/* 取引先の詳細（freeeの取引先情報にそのまま入る。空欄はfreee側の取引先台帳の値が使われる） */}
          <div><div style={th}>{t('cd.qs.partnerZip')}</div><input style={hdrInput} value={sheet.partnerZip || ''} onChange={e => setField('partnerZip', e.target.value)} placeholder="105-0021" /></div>
          <div style={{ gridColumn: 'span 2' }}><div style={th}>{t('cd.qs.partnerAddress1')}</div><input style={hdrInput} value={sheet.partnerAddress1 || ''} onChange={e => setField('partnerAddress1', e.target.value)} placeholder={t('cd.qs.partnerAddress1Ph')} /></div>
          <div><div style={th}>{t('cd.qs.partnerAddress2')}</div><input style={hdrInput} value={sheet.partnerAddress2 || ''} onChange={e => setField('partnerAddress2', e.target.value)} placeholder={t('cd.qs.partnerAddress2Ph')} /></div>
          <div><div style={th}>{t('cd.qs.partnerDept')}</div><input style={hdrInput} value={sheet.partnerDept || ''} onChange={e => setField('partnerDept', e.target.value)} placeholder={t('cd.qs.partnerDeptPh')} /></div>
        </div>
        <div style={{ overflowX: 'auto' }}>
          <table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 660 }}>
            <thead><tr>
              <th style={{ ...th, width: '38%' }}>{t('cd.qs.colName')}</th>
              <th style={{ ...th, width: 62, textAlign: 'right' }}>{t('cd.qs.colQty')}</th>
              <th style={{ ...th, width: 64 }}>{t('cd.qs.colUnit')}</th>
              <th style={{ ...th, width: 110, textAlign: 'right' }}>{t('cd.qs.colUnitPrice')}</th>
              <th style={{ ...th, width: 76 }}>{t('cd.qs.colTaxRate')}</th>
              <th style={{ ...th, width: 110, textAlign: 'right' }}>{t('cd.qs.colAmount')}</th>
              <th style={{ ...th, width: 76 }}></th>
            </tr></thead>
            <tbody>
              {sheet.items.length === 0 && (
                <tr><td colSpan={7} style={{ textAlign: 'center', color: '#b4bac3', fontSize: 12.5, padding: '18px 0' }}>{t('cd.qs.noItems')}</td></tr>
              )}
              {sheet.items.map((it, i) => {
                const isLast = i === sheet.items.length - 1;
                if (it.type === 'text') {
                  return (
                    <tr key={i}>
                      <td colSpan={6} style={{ padding: '3px 4px' }}><input style={{ ...cellInput, background: '#fafbfc', color: '#5a616c' }} value={it.name} onChange={e => setItem(i, 'name', e.target.value)} placeholder={t('cd.qs.textRowPlaceholder')} /></td>
                      {rowActions(i, isLast)}
                    </tr>
                  );
                }
                const amt = qsNum(it.qty) * qsNum(it.unitPrice);
                return (
                  <tr key={i}>
                    <td style={{ padding: '3px 4px' }}><input style={cellInput} value={it.name} onChange={e => setItem(i, 'name', e.target.value)} placeholder={t('cd.qs.featureNamePlaceholder')} /></td>
                    <td style={{ padding: '3px 4px' }}><input style={{ ...cellInput, textAlign: 'right' }} value={it.qty} onChange={e => setItem(i, 'qty', e.target.value)} inputMode="decimal" /></td>
                    <td style={{ padding: '3px 4px' }}><input style={cellInput} value={it.unit || ''} onChange={e => setItem(i, 'unit', e.target.value)} placeholder={window.t("extra.unit.set")} /></td>
                    <td style={{ padding: '3px 4px' }}><input style={{ ...cellInput, textAlign: 'right' }} value={it.unitPrice} onChange={e => setItem(i, 'unitPrice', e.target.value)} inputMode="numeric" placeholder="0" /></td>
                    <td style={{ padding: '3px 4px' }}>
                      <select style={{ ...cellInput, cursor: 'pointer', appearance: 'none', WebkitAppearance: 'none' }} value={it.taxRate} onChange={e => setItem(i, 'taxRate', Number(e.target.value))}>
                        {QS_TAX_OPTS.map(r => <option key={r} value={r}>{r}%</option>)}
                      </select>
                    </td>
                    <td style={{ padding: '3px 8px', textAlign: 'right', fontSize: 13, fontFamily: 'var(--mono)', color: '#2b2f38', whiteSpace: 'nowrap' }}>{qsYen(amt)}</td>
                    {rowActions(i, isLast)}
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
        <div style={{ display: 'flex', gap: 8, marginTop: 8 }}>
          <button onClick={() => addItem('item')} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, border: '1px dashed #d3d7de', background: '#fff', color: '#4a5af0', fontSize: 12.5, fontWeight: 600, borderRadius: 8, padding: '7px 12px', cursor: 'pointer', fontFamily: 'inherit' }}><Icon name="plus" size={13} stroke={2.4} />{t('cd.qs.addItemRow')}</button>
          <button onClick={() => addItem('text')} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, border: '1px dashed #d3d7de', background: '#fff', color: '#6b727c', fontSize: 12.5, fontWeight: 600, borderRadius: 8, padding: '7px 12px', cursor: 'pointer', fontFamily: 'inherit' }}><Icon name="plus" size={13} stroke={2.4} />{t('cd.qs.addTextRow')}</button>
        </div>

        <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 14 }}>
          <div style={{ width: 280, fontSize: 13 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', color: '#5a616c' }}><span>{t('cd.qs.subtotal')}</span><span style={{ fontFamily: 'var(--mono)' }}>{qsYen(subtotal)}</span></div>
            {taxRates.filter(r => r > 0).map(r => (
              <div key={r} style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 0', color: '#5a616c' }}>
                <span>{t('cd.qs.tax')}（{r}%{taxRates.length > 1 ? ` ${t('cd.qs.taxable')} ${qsYen(byRate[r].base)}` : ''}）</span>
                <span style={{ fontFamily: 'var(--mono)' }}>{qsYen(byRate[r].tax)}</span>
              </div>
            ))}
            <div style={{ display: 'flex', justifyContent: 'space-between', padding: '8px 0 0', marginTop: 4, borderTop: '1px solid #eef0f3', fontWeight: 700, color: '#1c1f26', fontSize: 15 }}><span>{t('cd.qs.total')}</span><span style={{ fontFamily: 'var(--mono)' }}>{qsYen(total)}</span></div>
          </div>
        </div>

        <div style={{ marginTop: 14 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <div style={{ ...th, padding: '0 8px 6px', flex: 1 }}>{t('cd.qs.note')}</div>
            {(D.quoteNoteTemplates || []).length > 0 && (
              <select value="" onChange={e => {
                const tpl = (D.quoteNoteTemplates || []).find(x => x.id === e.target.value);
                if (!tpl) return;
                if (String(sheet.note || '').trim() && !window.confirm(t('cd.qs.noteTplConfirm', { name: tpl.name }))) return;
                setField('note', tpl.text);   // 備考テンプレート（ナレッジ→見積もり用で管理）を丸ごと挿入
              }} style={{ flex: '0 0 auto', fontSize: 12, fontFamily: 'inherit', padding: '5px 8px', borderRadius: 8, border: '1px solid #dfe2e8', background: '#fff', color: '#4a5af0', marginBottom: 6 }}>
                <option value="">{t('cd.qs.noteTpl')}</option>
                {(D.quoteNoteTemplates || []).map(x => <option key={x.id} value={x.id}>{x.name}</option>)}
              </select>
            )}
          </div>
          {/* 行数は内容に追従（最低6行・従来3行の2倍〜。2026-08-29ユーザー要望） */}
          <textarea value={sheet.note || ''} onChange={e => setField('note', e.target.value)} rows={Math.min(20, Math.max(6, String(sheet.note || '').split('\n').length + 1))} placeholder={t('cd.qs.notePlaceholder')}
            style={{ ...cellInput, resize: 'vertical', lineHeight: 1.6 }} />
        </div>

        {isAdmin ? (
          <div style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid #f0f1f4', display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            {connected ? (
              <>
                <Button variant="primary" icon={sendBusy ? 'refresh' : 'inbox'} onClick={sendFreee} disabled={sendBusy}>{sendBusy ? t('cd.freeeCreating') : q ? t('cd.freeeRecreate') : t('cd.freeeCreate')}</Button>
                <span style={{ fontSize: 12, color: '#8a909a' }}>{q ? (q.number ? t('cd.freeeCreatedWithNo', { number: q.number }) : t('cd.freeeCreatedNoNumber')) : (freee.companyName ? t('cd.freeeSendTo', { name: freee.companyName }) : t('cd.freeeConnected'))}</span>
              </>
            ) : (
              <span style={{ fontSize: 12, color: '#9aa1ab' }}>
                <Icon name="inbox" size={13} stroke={2} style={{ color: '#2864f0', verticalAlign: '-2px', marginRight: 5 }} />
                {t('cd.freeeConnectPrefix')} <b>{t('cd.freeeConnectPath')}</b> {t('cd.freeeConnectSuffix')}
              </span>
            )}
          </div>
        ) : (
          <div style={{ marginTop: 16, paddingTop: 14, borderTop: '1px solid #f0f1f4', fontSize: 12, color: '#9aa1ab' }}>
            <Icon name="inbox" size={13} stroke={2} style={{ color: '#cbd0d7', verticalAlign: '-2px', marginRight: 5 }} />
            {t('cd.freeeAdminOnly')}
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, { QuoteSheet });
