// payment-request.jsx — generate a Payment Request (Giấy đề nghị thanh toán)
// PDF/printable document from a contract's payment-schedule installment.
// Company info đọc từ branding (white-label) sống ở main.jsx (p.branding), KHÔNG hardcode.

// Default company info (S.W.O) — dùng làm fallback khi branding thiếu trường.
const SWO_COMPANY = {
  name:'CÔNG TY TNHH CÔNG NGHỆ S.W.O',
  short:'S.W.O TECHNOLOGY',
  addr:'Số 4, Đường số 4, KDC Tân Thành Lập, P. Tân Mỹ, TP. Hồ Chí Minh',
  mst:'0315378557', phone:'08888 202 89', email:'info@swo.vn', web:'swo.vn',
  bank:'Vietcombank — CN TP. Hồ Chí Minh', stk:'0071 0000 20289',
  tagline:'Smart Works – Optimal & Simple',
  copyright:'', font:'Cambria',
};
// Font cho chứng từ → chuỗi font-family (kèm fallback). Dùng chung quote-doc & payment-request.
function docFontStack(co){ return "'" + ((co && co.font) || 'Cambria').replace(/'/g, '') + "','Times New Roman',serif"; }
// Danh sách font chứng từ (trùng select ở Thông tin công ty — branding.jsx) để chọn thay vì gõ tay.
const DOC_FONTS = ['Cambria', 'Times New Roman', 'Calibri', 'Arial', 'Tahoma', 'Georgia', 'Be Vietnam Pro'];

// Dựng object công ty cho chứng từ từ branding (live) + fallback SWO_COMPANY.
function docCompany(branding){
  const b = branding || {};
  return {
    name:  b.companyName || b.name || SWO_COMPANY.name,
    short: b.companyName || b.name || SWO_COMPANY.short,
    addr:  b.companyAddr || b.contactAddress || SWO_COMPANY.addr,
    logo:  b.docLogo || '',
    mst:   b.mst || SWO_COMPANY.mst,
    phone: b.contactPhone || SWO_COMPANY.phone,
    email: b.contactEmail || SWO_COMPANY.email,
    web:   b.contactWeb || SWO_COMPANY.web,
    bank:  b.bankName || SWO_COMPANY.bank,
    stk:   b.bankAcc || SWO_COMPANY.stk,
    font:  b.docFont || SWO_COMPANY.font,
    tagline: b.description || SWO_COMPANY.tagline,
    copyright: b.copyright || SWO_COMPANY.copyright,
  };
}

/* ============================================================================
   BIẾN & HEADER/FOOTER DÙNG CHUNG cho mọi chứng từ (hợp đồng · đề nghị TT · biên bản NT).
   - docVars(ctx): tập biến {TOKEN: giá trị} từ công ty + đối tác + ngữ cảnh.
   - applyDocVars / renderDocText: thay {TOKEN} (renderDocText còn escape an toàn).
   - docHeaderMotto / docHeaderNote / docFooter: dựng khối Quốc hiệu, dòng đầu trang, chân trang.
   ============================================================================ */
const DEFAULT_DOC_MOTTO_TITLE = 'CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM';
const DEFAULT_DOC_MOTTO_SUB   = 'Độc lập – Tự do – Hạnh phúc';
const DEFAULT_DOC_FOOTER      = '{TENVT} · {SLOGAN} · {EMAIL} · {DT}';
// Danh sách biến (token → mô tả) để hiển thị trợ giúp ở Cài đặt.
const DOC_VAR_HELP = [
  ['CONGTY','tên công ty'], ['TENVT','tên viết tắt'], ['MST','mã số thuế'], ['DIACHI','địa chỉ'],
  ['DT','điện thoại'], ['EMAIL','email'], ['WEB','website'], ['SLOGAN','slogan'],
  ['NGANHANG','ngân hàng'], ['STK','số tài khoản'], ['NGAY','ngày lập'], ['SO','số chứng từ'], ['KH','khách hàng / đối tác'],
];
const DOC_VAR_HELP_PAYREQ   = DOC_VAR_HELP.concat([['N','số đợt'], ['SOTIEN','số tiền đợt']]);
const DOC_VAR_HELP_CONTRACT = DOC_VAR_HELP.concat([['TONGTIEN','tổng giá trị HĐ'], ['BANG_HANG_HOA','bảng hàng hoá (tự chèn)'], ['LICH_THANH_TOAN','bảng lịch thanh toán (tự chèn)']]);

function docFlag(v, dflt){ return (v == null || v === '') ? !!dflt : (v === true || v === '1' || v === 1); }
function docEsc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, m => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[m])); }
function docDateVN(d){ const x = new Date(d || TODAY); return 'ngày ' + String(x.getDate()).padStart(2,'0') + ' tháng ' + String(x.getMonth()+1).padStart(2,'0') + ' năm ' + x.getFullYear(); }

function docVars(ctx){
  ctx = ctx || {}; const co = ctx.co || {};
  const v = {
    CONGTY: co.name || '', TENVT: co.short || '', MST: co.mst || '', DIACHI: co.addr || '',
    DT: co.phone || '', EMAIL: co.email || '', WEB: co.web || '', SLOGAN: co.tagline || '',
    NGANHANG: co.bank || '', STK: co.stk || '',
    NGAY: ctx.date || docDateVN(), SO: ctx.docNo || '', KH: (ctx.party && ctx.party.name) || '',
  };
  if (ctx.inst){ v.N = ctx.inst.no; v.SOTIEN = vnd(ctx.inst.amount); }
  if (ctx.contract && window.contractTotals){ v.TONGTIEN = vnd(contractTotals(ctx.contract).total); }
  return v;
}
// Thay {TOKEN} bằng giá trị; token không có giữ nguyên.
function applyDocVars(text, vars){ return String(text == null ? '' : text).replace(/\{([A-Z0-9_]+)\}/g, (m, k) => (vars && vars[k] != null) ? vars[k] : m); }
// Render an toàn: escape literal + escape giá trị biến (giữ {} để thay được).
function renderDocText(text, vars){ const ev = {}; Object.keys(vars || {}).forEach(k => ev[k] = docEsc(vars[k])); return applyDocVars(docEsc(text), ev); }
// Khối Quốc hiệu (phải header) · dòng note đầu trang (dưới divider) · chân trang.
function docHeaderMotto(tpl){ return tpl.showMotto ? `<div class="natl"><b>${docEsc(tpl.mottoTitle)}</b><br>${docEsc(tpl.mottoSub)}<br>———o0o———</div>` : ''; }
// Khối công ty ở header (logo + thông tin) — theo 2 cờ CHUNG docShowLogo / docShowCompany (áp cho cả 3 chứng từ).
function docHeaderCompany(co, S){
  S = S || {};
  const showLogo = docFlag(S.docShowLogo, true), showCo = docFlag(S.docShowCompany, true);
  if (!showLogo && !showCo) return '';
  const mark = (co.short || 'SWO').replace(/[^A-Za-z]/g, '').slice(0, 3).toUpperCase() || 'SWO';
  // Kích thước logo đặt INLINE (width/height + style, trùng .mark-img) để ảnh luôn đúng kích thước trên màn hình/PDF.
  const markEl = showLogo ? (co.logo ? `<img class="mark-img" src="${co.logo}" alt="logo" width="46" height="46" style="width:46px;height:46px;object-fit:contain;border-radius:8px">` : `<div class="mark" style="width:40px;height:40px">${mark}<i></i></div>`) : '';
  const info = showCo ? `<div><div class="n">${docEsc(co.name)}</div><div class="m">MST: ${docEsc(co.mst)} · ${docEsc(co.phone)} · ${docEsc(co.email)}</div><div class="m">${docEsc(co.addr)}</div></div>` : '';
  return `<div class="co">${markEl}${info}</div>`;
}
// Style INLINE cho khối .doc — font + cỡ chữ chứng từ theo cấu hình (docFont, docFontSize pt) áp lên bản in/PDF.
// font-family: theo cấu hình (fallback Times New Roman, serif). Cỡ chữ: docFontSize pt (mặc định 13).
function docDocStyle(co, S){
  S = S || {};
  const sz = (S.docFontSize != null && S.docFontSize !== '') ? Math.max(8, Math.min(20, parseFloat(S.docFontSize) || 13)) : 13;
  return `font-family:${docFontStack(co)};font-size:${sz}pt`;
}
// Chuyển text thường (có \n) → HTML giữ xuống dòng bằng <br> (Word KHÔNG hiểu white-space:pre-line → mất format).
function docTextToHtml(text, vars){ return renderDocText(text, vars).replace(/\n/g, '<br>'); }
// @page có số trang góc dưới-phải (CSS paged-media) — chèn vào <style> mỗi chứng từ. (Chrome screen-print chưa hỗ trợ margin-box.)
const DOC_PAGENUM_CSS = '@page{@bottom-right{content:"Trang " counter(page) "/" counter(pages);font-size:9pt;color:#98A2B3}}';
// Thanh header (khối công ty + Quốc hiệu) — chỉ render khi có nội dung (cả hai bị tắt → bỏ luôn đường kẻ header).
// Đường line phân cách header/nội dung: màu + độ dày (px) cấu hình qua docHeadLineColor / docHeadLineWidth.
// Đặt INLINE (Word cũng nhận) → ghi đè .head{border-bottom} mặc định.
function docHeadBar(co, S, mottoHtml){
  S = S || {};
  const inner = docHeaderCompany(co, S) + (mottoHtml || '');
  if (!inner.trim()) return '';
  const color = (String(S.docHeadLineColor || '').trim() || '#1466E0').replace(/[^#a-zA-Z0-9(),.% ]/g, '');
  const w = (S.docHeadLineWidth != null && S.docHeadLineWidth !== '') ? Math.max(0, parseFloat(S.docHeadLineWidth) || 0) : 2;
  return `<div class="head" style="border-bottom:${w}px solid ${color}">${inner}</div>`;
}
function docHeaderNote(tpl, vars){ return (tpl.headerNote && String(tpl.headerNote).trim()) ? `<div class="hdrnote">${renderDocText(tpl.headerNote, vars)}</div>` : ''; }
function docFooter(tpl, vars){ const f = (tpl.footer != null && String(tpl.footer).trim() !== '') ? tpl.footer : DEFAULT_DOC_FOOTER; return `<div class="foot">${renderDocText(f, vars)}</div>`; }
// CSS dùng chung cho dòng note đầu trang (chèn vào <style> mỗi chứng từ).
const DOC_HDRNOTE_CSS = '.hdrnote{margin:6px 0 2px;font-size:11.5px;color:#54657F;white-space:pre-line;text-align:center;font-style:italic}';

/* ============================================================================
   MẪU CHỨNG TỪ v2 — bố cục 2 cột (editor) + header 2 vùng + mã chèn + rich text.
   Model 1 JSON / loại chứng từ, lưu app_settings key `tpl<Loại>` (Contract/Quote/PayReq/Accept).
   value/onChange dùng qua setSetting (JSON.stringify) → docTpl() parse + fallback mặc định.
   Header KHÔNG Quốc hiệu cố định: 2 vùng trái/phải rich-text, chèn {LOGO}/{CONGTY}… tuỳ ý.
   ============================================================================ */
// Mã chèn (cột phải editor) — {LOGO} = logo công ty (raw) + các biến text (docVars).
const DOC_TPL_CODES = [['LOGO', 'logo công ty']].concat(DOC_VAR_HELP);
const DOC_TPL_CODES_CONTRACT = DOC_TPL_CODES.concat([['TONGTIEN', 'tổng giá trị HĐ'], ['BANG_HANG_HOA', 'bảng hàng hoá'], ['LICH_THANH_TOAN', 'bảng lịch thanh toán']]);
const DOC_TPL_CODES_PAYREQ   = DOC_TPL_CODES.concat([['N', 'số đợt'], ['SOTIEN', 'số tiền đợt']]);

// Logo công ty dạng HTML (ảnh thật hoặc ô chữ) ở kích thước px cấu hình — dùng cho {LOGO}.
function docLogoHtml(co, size){
  co = co || {};
  const s = Math.max(20, Math.min(120, parseInt(size, 10) || 46));
  const mark = ((co.short || 'SWO').replace(/[^A-Za-z]/g, '').slice(0, 3).toUpperCase()) || 'SWO';
  return co.logo
    ? `<img class="mark-img" src="${co.logo}" alt="logo" width="${s}" height="${s}" style="width:${s}px;height:${s}px;object-fit:contain;border-radius:8px;vertical-align:middle">`
    : `<span class="mark" style="width:${s}px;height:${s}px;display:inline-flex;flex-direction:column;align-items:center;justify-content:center;border-radius:${Math.round(s * .24)}px;background:linear-gradient(135deg,#12C5C0,#1466E0);color:#fff;font-weight:800;font-size:${Math.round(s * .3)}px;letter-spacing:.5px;vertical-align:middle">${mark}<i style="width:60%;height:2px;background:rgba(255,255,255,.9);border-radius:1px;margin-top:2px;display:block"></i></span>`;
}
// Render rich HTML: thay {CODE} = giá trị (escape GIÁ TRỊ, GIỮ markup) + {LOGO} = ảnh raw.
function renderRichVars(html, vars, co, logoSize){
  const ev = {}; Object.keys(vars || {}).forEach(k => ev[k] = docEsc(vars[k]));
  let out = applyDocVars(String(html == null ? '' : html), ev);
  return out.replace(/\{LOGO\}/g, docLogoHtml(co, logoSize));
}
// CSS header 2 vùng (letterhead) — cỡ chữ px CỐ ĐỊNH nhỏ (không scale theo cỡ nền) theo rule in A4.
const DOC_HEAD2_CSS = '.head2{display:flex;gap:16px;align-items:flex-start;padding-bottom:10px;margin-bottom:6px;font-size:11.5px;line-height:1.42;color:#54657F}.head2 .h2col{flex:1;min-width:0}.head2 .h2col b{color:#0A1730}.head2 .h2col p{margin:0}.head2 .mark-img,.head2 .mark{margin-bottom:3px}';
// Header 3 vùng (trái/giữa/phải, mỗi vùng có align riêng) + đường kẻ ngăn cách. cfg = tpl.header.
// Vùng GIỮA tuỳ chọn: trống → giữ layout 2 cột 50-50 như cũ; có nội dung → 3 cột đều nhau
// (flex:1 cả 3) nên cột giữa nằm ĐÚNG TÂM TRANG.
function docHeader2(co, S, cfg, vars){
  cfg = cfg || {};
  const Lc = cfg.left || {}, Cc = cfg.center || {}, Rc = cfg.right || {};
  const lh = renderRichVars(Lc.html || '', vars, co, cfg.logoSize);
  const ch = renderRichVars(Cc.html || '', vars, co, cfg.logoSize);
  const rh = renderRichVars(Rc.html || '', vars, co, cfg.logoSize);
  if (!String(lh).trim() && !String(ch).trim() && !String(rh).trim()) return '';
  const color = (String(cfg.lineColor || '#1466E0').trim().replace(/[^#a-zA-Z0-9(),.% ]/g, '')) || '#1466E0';
  const w = (cfg.lineWidth != null && cfg.lineWidth !== '') ? Math.max(0, parseFloat(cfg.lineWidth) || 0) : 2;
  const al = (a) => (a === 'center' || a === 'right') ? a : 'left';
  const mid = String(ch).trim() ? `<div class="h2col" style="text-align:${(Cc.align === 'left' || Cc.align === 'right') ? Cc.align : 'center'}">${ch}</div>` : '';
  return `<div class="head2" style="border-bottom:${w}px solid ${color}">`
    + `<div class="h2col" style="text-align:${al(Lc.align)}">${lh}</div>`
    + mid
    + `<div class="h2col" style="text-align:${al(Rc.align)}">${rh}</div></div>`;
}
// Header mặc định (giống letterhead cũ): trái = logo + công ty, giữa = trống, phải = Quốc hiệu (text — sửa/bỏ được).
function docDefaultHeader(){
  return {
    left:  { html: '{LOGO}<br><b>{CONGTY}</b><br>MST: {MST} · ĐT: {DT}<br>{DIACHI}', align: 'left' },
    center:{ html: '', align: 'center' },
    right: { html: '<b>CỘNG HÒA XÃ HỘI CHỦ NGHĨA VIỆT NAM</b><br>Độc lập – Tự do – Hạnh phúc<br>———o0o———', align: 'center' },
    lineColor: '#1466E0', lineWidth: 2, logoSize: 46,
  };
}
// text nhiều dòng (legacy \n) → HTML rich (giữ khi migrate mẫu cũ sang rich).
function docPlainToRich(t){ const s = String(t == null ? '' : t); return /<[a-z][\s\S]*>/i.test(s) ? s : s.replace(/[&<>]/g, m => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[m])).replace(/\n/g, '<br>'); }
// Nạp model template (parse JSON `tpl<Type>`; vắng → dựng từ key phẳng cũ + mặc định để KHÔNG mất tuỳ biến).
function docTpl(S, type, legacy){
  S = S || {}; legacy = legacy || {};
  const key = 'tpl' + type.charAt(0).toUpperCase() + type.slice(1);
  let raw = S[key];
  if (raw && typeof raw === 'string'){ try { raw = JSON.parse(raw); } catch (e) { raw = null; } }
  if (raw && typeof raw === 'object'){
    // partiesStacked mặc định theo LOẠI: biên bản (accept) = 2 cột cạnh nhau như trước (false),
    // hợp đồng = trên–dưới (true, đúng mẫu). Mẫu đã lưu có key riêng thì theo mẫu.
    return Object.assign({ header: docDefaultHeader(), title:'', subtitle:'', intro:'', partiesStacked:(type !== 'accept'), terms:[], body:'', signA:'BÊN A', signB:'BÊN B', footer:'', config:{} }, raw,
      { header: Object.assign(docDefaultHeader(), raw.header || {}), config: Object.assign({ font:'', fontSize:13, textColor:'#0A1730', accent:'#1466E0' }, raw.config || {}) });
  }
  // Migrate từ key phẳng cũ (legacy = { title, subtitle, intro, terms, body, signA, signB, footer })
  return {
    header: docDefaultHeader(),
    title: legacy.title || '', subtitle: legacy.subtitle || '',
    intro: docPlainToRich(legacy.intro || ''),
    partiesStacked: docFlag(S[legacy.stackedKey], type !== 'accept'),
    terms: legacy.terms != null ? [{ title:'', content: docPlainToRich(legacy.terms) }] : [],
    body: docPlainToRich(legacy.body || ''),
    signA: legacy.signA || 'BÊN A', signB: legacy.signB || 'BÊN B',
    footer: docPlainToRich(legacy.footer != null ? legacy.footer : DEFAULT_DOC_FOOTER),
    config: { font: S.docFont || '', fontSize: S.docFontSize || 13, textColor:'#0A1730', accent: S.docHeadLineColor || '#1466E0' },
  };
}
// Style .doc theo config (font + cỡ nền pt + màu chữ) của model v2.
// Font: ưu tiên font RIÊNG của mẫu (config.font, chọn từ DOC_FONTS) → trống thì theo công ty (co.font).
function docTplStyle(co, tpl){
  const c = (tpl && tpl.config) || {};
  const sz = Math.max(8, Math.min(20, parseFloat(c.fontSize) || 13));
  const col = (String(c.textColor || '#0A1730').replace(/[^#a-zA-Z0-9(),.% ]/g, '')) || '#0A1730';
  const font = String(c.font || '').trim() || (co && co.font);
  return `font-family:${docFontStack({ font })};font-size:${sz}pt;color:${col}`;
}

/* ---- Editor mẫu chứng từ v2 (2 cột) — dùng chung Hợp đồng/Báo giá/Đề nghị TT/Biên bản ----
   RichEditor (email.jsx) = định dạng cơ bản (đậm/nghiêng/gạch/màu/list/link) — KHÔNG đổi cỡ/font → in đồng bộ.
   Style inline (tránh đụng layout.css đang do session khác sửa). */
function TplRich({ label, hint, value, onChange }){
  return <Field label={label} hint={hint}><RichEditor html={value || ''} onInput={onChange} /></Field>;
}
// Danh sách MÃ chèn (nút copy) — cột phải.
function TplCodeList({ codes }){
  const { lang } = useLang(); const toast = useToast();
  const copy = (code) => { const s = '{' + code + '}'; try { navigator.clipboard && navigator.clipboard.writeText(s); } catch (e) {} toast((lang === 'vi' ? 'Đã copy ' : 'Copied ') + s, 'success'); };
  return (
    <div className="col" style={{ gap:4 }}>
      {codes.map(([code, desc]) => (
        <button type="button" key={code} onClick={() => copy(code)} title={lang === 'vi' ? 'Copy để chèn vào ô' : 'Copy to insert'}
          style={{ display:'flex', alignItems:'center', gap:8, width:'100%', textAlign:'left', padding:'5px 8px', border:'1px solid var(--border)', background:'var(--bg)', borderRadius:'var(--r-md)', cursor:'pointer', fontSize:'var(--fs-base)' }}>
          <span className="mono" style={{ color:'var(--primary)', fontWeight:600 }}>{'{' + code + '}'}</span>
          <span className="grow truncate" style={{ color:'var(--text-muted)' }}>{desc}</span>
          <i className="bi bi-clipboard" style={{ color:'var(--text-subtle)' }}></i>
        </button>
      ))}
    </div>
  );
}
// Bảng cấu hình (font, cỡ nền, màu, logo, đường kẻ) — cột phải.
function TplConfigPanel({ tpl, set }){
  const { lang } = useLang(); const L = (vi, en) => lang === 'vi' ? vi : en;
  const cfg = tpl.config || {}, h = tpl.header || {};
  const setCfg = (k, v) => set({ ...tpl, config:{ ...cfg, [k]:v } });
  const setH = (k, v) => set({ ...tpl, header:{ ...h, [k]:v } });
  const colorBox = { padding:2, height:'var(--control-h)', width:'100%' };
  return (
    <div className="col g10">
      <div className="sys-sec-title"><i className="bi bi-sliders"></i>{L('Cấu hình chung', 'Config')}</div>
      <div className="flex g8 wrap items-start">
        <div style={{ flex:'1 1 110px' }}><Field label={L('Cỡ chữ nền (pt)', 'Font size (pt)')}><input className="input" type="number" min="8" max="20" value={cfg.fontSize != null ? cfg.fontSize : 13} onChange={e => setCfg('fontSize', e.target.value)} /></Field></div>
        <div style={{ flex:'0 0 84px' }}><Field label={L('Màu chữ', 'Text color')}><input className="input" type="color" value={cfg.textColor || '#0A1730'} onChange={e => setCfg('textColor', e.target.value)} style={colorBox} /></Field></div>
      </div>
      <Field label={L('Font chữ', 'Font')} hint={L('Mặc định = theo Thông tin công ty', 'Default = company setting')}>
        <select className="select" value={cfg.font || ''} onChange={e => setCfg('font', e.target.value)} style={{ fontFamily: cfg.font ? "'" + cfg.font + "',serif" : undefined }}>
          <option value="">{L('— Theo Thông tin công ty —', '— Company default —')}</option>
          {/* font gõ tay từ bản cũ không có trong list → vẫn giữ làm option để không mất cấu hình */}
          {(cfg.font && !DOC_FONTS.includes(cfg.font) ? [cfg.font].concat(DOC_FONTS) : DOC_FONTS).map(fn => <option key={fn} value={fn} style={{ fontFamily:"'" + fn + "',serif" }}>{fn}</option>)}
        </select>
      </Field>
      <div className="sys-sec-title" style={{ marginTop:4 }}><i className="bi bi-image"></i>{L('Header', 'Header')}</div>
      <div className="flex g8 wrap items-start">
        <div style={{ flex:'1 1 100px' }}><Field label={L('Cỡ logo (px)', 'Logo (px)')}><input className="input" type="number" min="20" max="120" value={h.logoSize != null ? h.logoSize : 46} onChange={e => setH('logoSize', e.target.value)} /></Field></div>
        <div style={{ flex:'0 0 84px' }}><Field label={L('Màu kẻ', 'Line')}><input className="input" type="color" value={h.lineColor || '#1466E0'} onChange={e => setH('lineColor', e.target.value)} style={colorBox} /></Field></div>
        <div style={{ flex:'0 0 80px' }}><Field label={L('Dày (px)', 'Width')}><input className="input" type="number" min="0" max="8" value={h.lineWidth != null ? h.lineWidth : 2} onChange={e => setH('lineWidth', e.target.value)} /></Field></div>
      </div>
    </div>
  );
}
// Sửa 3 vùng header (trái/giữa/phải) + align mỗi vùng. Vùng GIỮA tuỳ chọn — trống thì
// chứng từ in 2 cột như cũ; có nội dung thì cột giữa căn ĐÚNG TÂM TRANG.
function TplHeaderEditor({ tpl, set }){
  const { lang } = useLang(); const L = (vi, en) => lang === 'vi' ? vi : en;
  const h = tpl.header || {};
  const setSide = (side, k, v) => set({ ...tpl, header:{ ...h, [side]:{ ...(h[side] || {}), [k]:v } } });
  const AlignSel = ({ side, def }) => (
    <select className="select" style={{ width:104, height:26, fontSize:'var(--fs-sm)' }} value={(h[side] || {}).align || def || 'left'} onChange={e => setSide(side, 'align', e.target.value)}>
      <option value="left">{L('Căn trái', 'Left')}</option><option value="center">{L('Căn giữa', 'Center')}</option><option value="right">{L('Căn phải', 'Right')}</option>
    </select>
  );
  const col = (side, label, opts) => (
    <div style={{ flex:'1 1 240px', minWidth:0 }}>
      <div className="flex justify-between items-center" style={{ marginBottom:5 }}>
        <span style={{ fontSize:'var(--fs-base)', fontWeight:500, color:'var(--text-muted)' }}>{label}</span><AlignSel side={side} def={opts && opts.def} />
      </div>
      <RichEditor html={(h[side] || {}).html || ''} onInput={v => setSide(side, 'html', v)} placeholder={(opts && opts.ph) || L('Chèn mã {LOGO}, {CONGTY}…', 'Insert {LOGO}, {CONGTY}…')} />
    </div>
  );
  return <div className="flex g12 wrap items-start">
    {col('left', L('Header — cột trái', 'Header — left'))}
    {col('center', L('Header — ở giữa (tuỳ chọn)', 'Header — center (optional)'), { def:'center', ph:L('Trống = bố cục 2 cột như cũ', 'Blank = 2-column layout') })}
    {col('right', L('Header — cột phải', 'Header — right'))}
  </div>;
}
// Điều khoản dạng mục {title, content} — thêm/xoá/sửa/đổi thứ tự.
function TplTermsEditor({ terms, set }){
  const { lang } = useLang(); const L = (vi, en) => lang === 'vi' ? vi : en;
  const list = Array.isArray(terms) ? terms : [];
  const upd = (i, k, v) => set(list.map((t, j) => j === i ? { ...t, [k]:v } : t));
  const add = () => set(list.concat([{ title:(lang === 'vi' ? 'Điều ' : 'Article ') + (list.length + 1) + ': ', content:'' }]));
  const del = (i) => set(list.filter((_, j) => j !== i));
  const move = (i, d) => { const j = i + d; if (j < 0 || j >= list.length) return; const a = list.slice(); const t = a[i]; a[i] = a[j]; a[j] = t; set(a); };
  return (
    <div className="col g10">
      {list.map((t, i) => (
        <div key={i} style={{ border:'1px solid var(--border)', borderRadius:'var(--r-lg)', padding:'10px 12px', background:'var(--bg-subtle)' }}>
          <div className="flex g8 items-center" style={{ marginBottom:6 }}>
            <span className="mono" style={{ width:22, height:22, flex:'none', display:'flex', alignItems:'center', justifyContent:'center', borderRadius:'var(--r-sm)', background:'var(--primary-light)', color:'var(--primary-ink)', fontWeight:700, fontSize:'var(--fs-sm)' }}>{i + 1}</span>
            <input className="input" style={{ flex:1, fontWeight:600 }} value={t.title || ''} onChange={e => upd(i, 'title', e.target.value)} placeholder={L('Tiêu đề (VD: Điều 1: Nội dung công việc)', 'Title (e.g. Article 1)')} />
            <button className="iconbtn" title={L('Lên', 'Up')} onClick={() => move(i, -1)} disabled={i === 0}><i className="bi bi-chevron-up"></i></button>
            <button className="iconbtn" title={L('Xuống', 'Down')} onClick={() => move(i, 1)} disabled={i === list.length - 1}><i className="bi bi-chevron-down"></i></button>
            <button className="iconbtn" title={L('Xoá', 'Delete')} onClick={() => del(i)}><i className="bi bi-trash"></i></button>
          </div>
          <RichEditor html={t.content || ''} onInput={v => upd(i, 'content', v)} placeholder={L('Nội dung điều khoản… (chèn {BANG_HANG_HOA} / {LICH_THANH_TOAN} nếu cần)', 'Article content…')} />
        </div>
      ))}
      <button className="btn btn-subtle btn-sm" style={{ alignSelf:'flex-start' }} onClick={add}><i className="bi bi-plus-lg"></i>{L('Thêm điều khoản', 'Add article')}</button>
    </div>
  );
}
// Khung 2 cột 70/30 (trái = nội dung, phải = cấu hình + mã). Xuống 1 cột khi hẹp.
function TplBuilder({ children, side }){
  return (
    <div style={{ display:'grid', gridTemplateColumns:'minmax(0,7fr) minmax(0,3fr)', gap:16, alignItems:'flex-start' }} className="tpl-builder-grid">
      <div className="col g14" style={{ minWidth:0 }}>{children}</div>
      <div style={{ position:'sticky', top:8, alignSelf:'start', display:'flex', flexDirection:'column', gap:14, minWidth:0 }}>{side}</div>
    </div>
  );
}
// Tiêu đề mục nhỏ trong editor (khỏi phụ thuộc SecTitle của module khác).
function TplSec({ children }){ return <div style={{ fontSize:'var(--fs-md)', fontWeight:700, color:'var(--text)', margin:'6px 0 -2px', paddingTop:6, borderTop:'1px solid var(--border-light)' }}>{children}</div>; }
// Editor mẫu chứng từ 2 cột — dùng chung cho cả 4 loại. type ∈ contract/quote/payreq/accept.
// legacy = key phẳng cũ để migrate; previewHtml(liveS) → HTML xem trước (caller cấp vì cần renderer + sample + branding).
function DocTemplateEditor({ label, type, codes, hasTerms, bodyLabel, showIntro, showSigns, showParties, legacy, previewHtml, fileName }){
  showIntro = showIntro !== false; showSigns = showSigns !== false;
  // showParties: hiện ô "Bố cục Bên A / Bên B" — mặc định theo showSigns (HĐ có, báo giá/ĐNTT không);
  // Biên bản bàn giao truyền riêng showParties={true} (có khối Bên giao/Bên nhận nhưng không sửa ô ký).
  const showPartiesF = showParties != null ? showParties : showSigns;
  const p = usePortal(); const { t, lang } = useLang(); const toast = useToast();
  const L = (vi, en) => lang === 'vi' ? vi : en;
  const cap = type.charAt(0).toUpperCase() + type.slice(1);
  const [m, setM] = React.useState(() => docTpl(p.appSettings, type, legacy || {}));
  const [pv, setPv] = React.useState(false);
  const set = (nm) => setM(nm);
  const setK = (k, v) => setM(s => ({ ...s, [k]:v }));
  const save = () => { p.setSetting('tpl' + cap, JSON.stringify(m)); toast(L('Đã lưu mẫu', 'Template saved'), 'success'); };
  const liveS = { ...p.appSettings, ['tpl' + cap]: JSON.stringify(m) };
  const actions = <div className="flex g8"><button className="btn" onClick={() => setPv(true)}><i className="bi bi-eye"></i>{L('Xem trước', 'Preview')}</button><button className="btn btn-primary" onClick={save}>{t('c.save')}</button></div>;
  return (
    <div className="card">
      <div className="card-head"><h3>{label}</h3>{actions}</div>
      <div className="card-body">
        <TplBuilder side={<>
          <TplConfigPanel tpl={m} set={set} />
          <div><div className="sys-sec-title"><i className="bi bi-braces"></i>{L('Mã chèn (bấm để copy)', 'Codes (click to copy)')}</div>
            <div className="subtle" style={{ fontSize:'var(--fs-sm)', margin:'2px 0 6px' }}>{L('Copy rồi dán vào ô bất kỳ. {LOGO} chèn logo.', 'Copy then paste into any box. {LOGO} inserts the logo.')}</div>
            <TplCodeList codes={codes} /></div>
        </>}>
          <TplSec>{L('Header — 3 vùng: trái · giữa · phải (chèn {LOGO}, {CONGTY}… tuỳ ý)', 'Header — 3 areas: left · center · right')}</TplSec>
          <TplHeaderEditor tpl={m} set={set} />
          <TplSec>{L('Nội dung', 'Content')}</TplSec>
          <div className="flex g12 wrap items-start">
            <div style={{ flex:1, minWidth:200 }}><Field label={L('Tiêu đề', 'Title')}><input className="input" value={m.title || ''} onChange={e => setK('title', e.target.value)} /></Field></div>
            <div style={{ flex:1, minWidth:200 }}><Field label={L('Phụ đề (tuỳ chọn)', 'Subtitle')}><input className="input" value={m.subtitle || ''} onChange={e => setK('subtitle', e.target.value)} /></Field></div>
          </div>
          {showIntro && <TplRich label={L('Lời mở đầu / căn cứ', 'Preamble / basis')} value={m.intro} onChange={v => setK('intro', v)} />}
          {/* Bố cục khối thông tin Bên A / Bên B — đặt đúng vị trí xuất hiện trong văn bản
              (sau lời mở đầu, trước điều khoản), KHÔNG nằm chung mục Ô ký. */}
          {showPartiesF && <Field label={L('Bố cục Bên A / Bên B', 'Party layout')}>
            <label className="flex g8 items-center" style={{ height:'var(--control-h)', cursor:'pointer' }}><input type="checkbox" checked={!!m.partiesStacked} onChange={e => setK('partiesStacked', e.target.checked)} />{L('Xếp trên–dưới', 'Stacked')}</label>
          </Field>}
          {hasTerms && <><TplSec>{L('Điều khoản (mỗi mục: tiêu đề + nội dung)', 'Terms (articles)')}</TplSec><TplTermsEditor terms={m.terms} set={ts => setK('terms', ts)} /></>}
          {/* TplSec đã là tiêu đề mục → TplRich bên dưới KHÔNG lặp lại label (tránh nhãn đôi dính nhau) */}
          {bodyLabel && <><TplSec>{bodyLabel}</TplSec><TplRich label="" value={m.body} onChange={v => setK('body', v)} /></>}
          {showSigns && <><TplSec>{L('Ô ký Bên A / Bên B', 'Signatures')}</TplSec>
          <div className="flex g12 wrap items-start">
            <div style={{ flex:1, minWidth:160 }}><Field label={L('Ô ký Bên A', 'Sign A')}><input className="input" value={m.signA || ''} onChange={e => setK('signA', e.target.value)} /></Field></div>
            <div style={{ flex:1, minWidth:160 }}><Field label={L('Ô ký Bên B', 'Sign B')}><input className="input" value={m.signB || ''} onChange={e => setK('signB', e.target.value)} /></Field></div>
          </div></>}
          <TplSec>{L('Chân trang', 'Footer')}</TplSec>
          <TplRich label="" value={m.footer} onChange={v => setK('footer', v)} />
          <div className="flex justify-end">{actions}</div>
        </TplBuilder>
      </div>
      {pv && <DocPreviewModal title={L('Xem trước · ', 'Preview · ') + label} html={previewHtml(liveS)} fileName={fileName} onClose={() => setPv(false)} />}
    </div>
  );
}

/* ---- Modal xem trước chung (preview + In/PDF) — dùng ở Cài đặt & các chứng từ ---- */
function DocPreviewModal({ title, html, fileName, sampleNote, onClose }){
  const { t, lang } = useLang();
  const toast = useToast();
  const doPrint = () => { if (!printDocument(html)) toast(lang === 'vi' ? 'Trình duyệt chặn cửa sổ in — hãy cho phép pop-up' : 'Pop-up blocked — allow pop-ups', 'error'); };
  return (
    <Modal title={title} width={840} onClose={onClose}
      footer={<><div className="grow flex g8 items-center subtle" style={{ fontSize:'var(--fs-sm)' }}>{sampleNote ? <><i className="bi bi-info-circle"></i>{sampleNote}</> : null}</div>
        <button className="btn" onClick={onClose}>{t('c.close')}</button>
        <button className="btn btn-primary" onClick={doPrint}><i className="bi bi-printer"></i>{lang === 'vi' ? 'In / PDF' : 'Print / PDF'}</button></>}>
      <iframe title="preview" srcDoc={html} style={{ width:'100%', height:'62vh', border:'1px solid var(--border)', borderRadius:'var(--r-md)', background:'#fff' }}></iframe>
    </Modal>
  );
}

/* ---- Dữ liệu minh hoạ cho preview ở Cài đặt (không đụng dữ liệu thật) ---- */
const DOC_SAMPLE_CONTRACT = {
  code:'2806-2026-SWO-KZN', contractNo:'2806/2026/SWO-KZN', signDate:TODAY, customer:'__demo__', rep:'__demo__', direction:'sell', category:'',
  items:[
    { name:'Triển khai hệ thống phần mềm quản lý', unit:'Gói', qty:1, price:80000000, vat:8 },
    { name:'Đào tạo & chuyển giao vận hành', unit:'Buổi', qty:2, price:5000000, vat:8 },
  ],
  schedule:[
    { no:1, amount:48600000, dueDate:TODAY, condition:'Tạm ứng khi ký hợp đồng', paid:true, paidDate:TODAY },
    { no:2, amount:48600000, dueDate:TODAY, condition:'Sau khi nghiệm thu & bàn giao', paid:false },
  ],
};
const DOC_SAMPLE_INST = { no:2, amount:48600000, dueDate:TODAY, condition:'Sau khi nghiệm thu & bàn giao' };
const DOC_SAMPLE_PARTY = { name:'CÔNG TY TNHH KIZUNA VIỆT NAM', shortName:'KIZUNA', mst:'0312345678', address:'Đường số 1, KCN Tân Phú Trung, TP. Hồ Chí Minh' };
const DOC_SAMPLE_REP   = { name:'Nguyễn Văn An', title:'Giám đốc' };

/* ---- Mẫu Giấy đề nghị thanh toán (sửa được ở Cài đặt hợp đồng → app_settings) ---- */
const DEFAULT_PAYREQ_TITLE = 'GIẤY ĐỀ NGHỊ THANH TOÁN';
const DEFAULT_PAYREQ_INTRO = 'Đề nghị Quý khách thanh toán đợt {N} theo lịch thanh toán của hợp đồng, chi tiết như sau:';
const DEFAULT_PAYREQ_NOTE = '';
const DEFAULT_PAYREQ_SIGN = ['NGƯỜI ĐỀ NGHỊ', 'KẾ TOÁN TRƯỞNG', 'GIÁM ĐỐC'];
function paymentRequestTemplate(S){
  S = S || {};
  return {
    title: (S.payReqTitle || '').trim() || DEFAULT_PAYREQ_TITLE,
    intro: (S.payReqIntro != null ? S.payReqIntro : DEFAULT_PAYREQ_INTRO),
    note:  (S.payReqNote != null ? S.payReqNote : DEFAULT_PAYREQ_NOTE),
    sign1: (S.payReqSign1 || '').trim() || DEFAULT_PAYREQ_SIGN[0],
    sign2: (S.payReqSign2 || '').trim() || DEFAULT_PAYREQ_SIGN[1],
    sign3: (S.payReqSign3 || '').trim() || DEFAULT_PAYREQ_SIGN[2],
    // Header / footer (sửa được + biến)
    showMotto:  docFlag(S.payReqShowMotto, true),
    mottoTitle: (S.payReqMottoTitle || '').trim() || DEFAULT_DOC_MOTTO_TITLE,
    mottoSub:   (S.payReqMottoSub != null ? S.payReqMottoSub : DEFAULT_DOC_MOTTO_SUB),
    headerNote: (S.payReqHeaderNote != null ? S.payReqHeaderNote : ''),
    footer:     (S.payReqFooter != null ? S.payReqFooter : DEFAULT_DOC_FOOTER),
  };
}

/* Vietnamese number-to-words (đọc số tiền thành chữ) */
function docTienChu(num){
  num = Math.round(num || 0);
  if (num === 0) return 'Không đồng';
  const dv = ['không','một','hai','ba','bốn','năm','sáu','bảy','tám','chín'];
  const scale = ['','nghìn','triệu','tỷ'];
  function read3(n, showHundred){
    const tr = Math.floor(n / 100), ch = Math.floor((n % 100) / 10), u = n % 10;
    const parts = [];
    if (tr > 0 || showHundred) parts.push(dv[tr] + ' trăm');
    if (ch > 1){ parts.push(dv[ch] + ' mươi'); if (u === 1) parts.push('mốt'); else if (u === 5) parts.push('lăm'); else if (u > 0) parts.push(dv[u]); }
    else if (ch === 1){ parts.push('mười'); if (u === 1) parts.push('một'); else if (u === 5) parts.push('lăm'); else if (u > 0) parts.push(dv[u]); }
    else if (u > 0){ if (tr > 0 || showHundred) parts.push('lẻ'); parts.push(dv[u]); }
    return parts.join(' ');
  }
  const groups = []; let x = num;
  while (x > 0){ groups.push(x % 1000); x = Math.floor(x / 1000); }
  const words = [];
  for (let i = groups.length - 1; i >= 0; i--){
    if (groups[i] === 0) continue;
    words.push((read3(groups[i], i < groups.length - 1) + ' ' + scale[i]).trim());
  }
  let s = words.join(' ').replace(/\s+/g, ' ').trim();
  return s.charAt(0).toUpperCase() + s.slice(1) + ' đồng';
}

/* Build the printable HTML document for one installment */
function paymentRequestHTML(c, inst, cust, rep, totalContract, branding, S){
  const reqNo = 'YCTT-' + c.code + '-D' + inst.no;
  const tpl = paymentRequestTemplate(S);
  const esc = (s) => String(s == null ? '' : s).replace(/[&<>]/g, m => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;' }[m]));
  const today = new Date(TODAY);
  const dateLine = 'TP. Hồ Chí Minh, ngày ' + String(today.getDate()).padStart(2, '0') + ' tháng ' + String(today.getMonth() + 1).padStart(2, '0') + ' năm ' + today.getFullYear();
  const co = docCompany(branding);
  const vars = docVars({ co, party:cust, docNo:reqNo, date:docDateVN(), inst });
  // Khung mẫu v2 (header 2 vùng · config · title · footer); THÂN đề nghị TT giữ riêng (câu đề nghị · số tiền · TK).
  const V = docTpl(S, 'payreq', { title:tpl.title, subtitle:'', body:tpl.intro, footer:(S.payReqFooter != null ? S.payReqFooter : DEFAULT_DOC_FOOTER) });
  const logoSz = (V.header || {}).logoSize;
  const renderRP = (src) => /<[a-z][\s\S]*>/i.test(src || '') ? renderRichVars(src, vars, co, logoSz) : docTextToHtml(src, vars);
  const introHtml = renderRP(V.body || tpl.intro);   // câu đề nghị (sửa ở editor v2, {N}/{KH} tự thay)
  return `<!DOCTYPE html><html lang="vi"><head><meta charset="utf-8"><title>${reqNo}</title>
<style>
@page{size:A4;margin:16mm}
${DOC_PAGENUM_CSS}
${DOC_HEAD2_CSS}
*{box-sizing:border-box}
body{font-family:${docFontStack(co)};color:#0A1730;font-size:13px;line-height:1.5;margin:0}
.doc{max-width:720px;margin:0 auto;padding:8px}
.head{display:flex;justify-content:space-between;align-items:flex-start;border-bottom:2px solid #1466E0;padding-bottom:10px;margin-bottom:6px}
.co{display:flex;gap:10px;align-items:center}
.mark{width:40px;height:40px;border-radius:11px;background:linear-gradient(135deg,#12C5C0,#1466E0);color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;font-weight:800;font-size:13px;letter-spacing:.5px}
.mark i{width:60%;height:2px;background:rgba(255,255,255,.9);border-radius:1px;margin-top:2px;display:block}
.mark-img{width:46px;height:46px;object-fit:contain;border-radius:8px}
/* Cỡ chữ NỀN = .doc (docDocStyle, docFontSize pt) — body text KHÔNG đặt px, phân cấp bằng em → KHÔNG nhảy font.
   Riêng KHỐI ĐẦU TRANG (letterhead công ty + Quốc hiệu) giữ px nhỏ CỐ ĐỊNH (không scale theo cỡ nền) để khỏi tràn dòng. */
.co .n{font-weight:700;font-size:14px}
.co .m{font-size:11px;color:#54657F}
.natl{text-align:center;font-size:11px;color:#54657F}
.natl b{color:#0A1730}
h1{text-align:center;font-size:1.5em;letter-spacing:.5px;margin:18px 0 2px;color:#0A1730}
.sub{text-align:center;font-size:.85em;color:#54657F;margin-bottom:18px}
.row{display:flex;margin:3px 0}
.row .k{width:170px;color:#54657F}
.row .v{font-weight:600;flex:1}
table{width:100%;border-collapse:collapse;margin:12px 0}
th,td{border:1px solid #D0D5DD;padding:7px 9px;text-align:left}
th{background:#EAF1FE;font-weight:700}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
.amount-box{background:#F6F9FD;border:1px solid #D2E1FD;border-radius:8px;padding:12px 14px;margin:14px 0}
.amount-box .big{font-size:1.7em;font-weight:800;color:#1466E0}
.amount-box .words{font-style:italic;margin-top:4px}
.bank{margin:10px 0;padding:10px 12px;border-left:3px solid #12C5C0;background:#F6F9FD}
.prnote{white-space:pre-line;margin:12px 0;color:#0A1730}
.sign{display:flex;justify-content:space-between;margin-top:34px;text-align:center}
.sign>div{width:31%}
.sign .role{font-weight:700}
.sign .note{font-size:.8em;color:#54657F;font-style:italic}
.sign .space{height:64px}
.foot{margin-top:24px;border-top:1px solid #E3E6EC;padding-top:8px;font-size:.8em;color:#98A2B3;text-align:center;white-space:pre-line}
${DOC_HDRNOTE_CSS}
@media print{.no-print{display:none!important}.doc{max-width:none;margin:0;padding:0}}
</style></head>
<body><div class="doc" style="${docTplStyle(co, V)}">
  ${docHeader2(co, S, V.header, vars)}

  <h1>${esc(V.title || tpl.title)}</h1>
  ${(V.subtitle || '').trim() ? `<div class="sub" style="margin-bottom:2px">${esc(V.subtitle)}</div>` : ''}
  <div class="sub">Số: ${reqNo} &nbsp;·&nbsp; ${dateLine}</div>

  <div class="row"><div class="k">Kính gửi:</div><div class="v">${cust.name || '—'}</div></div>
  <div class="row"><div class="k">Mã số thuế:</div><div class="v">${cust.mst || '—'}</div></div>
  <div class="row"><div class="k">Địa chỉ:</div><div class="v">${cust.address || '—'}</div></div>
  <div class="row"><div class="k">Người đại diện:</div><div class="v">${rep.name || '—'}${rep.title ? ' — ' + rep.title : ''}</div></div>
  <div class="row"><div class="k">Căn cứ hợp đồng số:</div><div class="v">${c.contractNo || c.code} (ký ngày ${fmtDate(c.signDate)}) — Tổng giá trị: ${vnd(totalContract)}</div></div>

  <p style="margin:14px 0 6px">${introHtml}</p>
  <table>
    <thead><tr><th style="width:44px">Đợt</th><th>Điều kiện / nội dung</th><th style="width:110px">Ngày đến hạn</th><th class="num" style="width:150px">Số tiền (VNĐ)</th></tr></thead>
    <tbody><tr><td style="text-align:center">${inst.no}</td><td>${inst.condition || 'Thanh toán theo hợp đồng'}</td><td>${fmtDate(inst.dueDate)}</td><td class="num"><b>${vnd(inst.amount).replace('₫','')}</b></td></tr></tbody>
  </table>

  <div class="amount-box">
    <div class="big">${vnd(inst.amount)}</div>
    <div class="words">Bằng chữ: ${docTienChu(inst.amount)}.</div>
  </div>

  <div class="bank"><b>Thông tin chuyển khoản:</b><br>Ngân hàng: ${co.bank} · Số TK: ${co.stk}<br>Chủ tài khoản: ${co.name}<br>Nội dung: <b>${reqNo} ${cust.shortName || ''}</b></div>

  ${tpl.note && tpl.note.trim() ? `<div class="prnote">${esc(tpl.note).replace(/\n/g, '<br>')}</div>` : ''}

  <div class="sign">
    <div><div class="role">${esc(tpl.sign1)}</div><div class="note">(Ký, ghi rõ họ tên)</div><div class="space"></div></div>
    <div><div class="role">${esc(tpl.sign2)}</div><div class="note">(Ký, ghi rõ họ tên)</div><div class="space"></div></div>
    <div><div class="role">${esc(tpl.sign3)}</div><div class="note">(Ký, đóng dấu)</div><div class="space"></div></div>
  </div>

  <div class="foot">${renderRP(V.footer)}</div>
</div></body></html>`;
}

function printDocument(html){
  const w = window.open('', '_blank', 'width=900,height=1000');
  if (!w){ return false; }
  w.document.open(); w.document.write(html); w.document.close();
  w.focus();
  setTimeout(() => { try { w.print(); } catch(e){} }, 350);
  return true;
}

function PaymentRequestModal({ contract, inst, onClose }){
  const p = usePortal();
  const { t, lang } = useLang();
  const toast = useToast();
  const cust = (window.CustomerStore && CustomerStore.byCode(contract.customer)) || {};
  const rep = contract.rep && window.ContactStore ? (ContactStore.byCode(contract.rep) || {}) : {};
  const total = contractTotals(contract).total;
  const html = paymentRequestHTML(contract, inst, cust, rep, total, p.branding, p.appSettings);
  const reqNo = 'YCTT-' + contract.code + '-D' + inst.no;
  const doPrint = () => { if (!printDocument(html)) toast(lang === 'vi' ? 'Trình duyệt chặn cửa sổ in — hãy cho phép pop-up' : 'Pop-up blocked — allow pop-ups', 'error'); };
  return (
    <Modal title={t('hd.payReq') + ' · ' + reqNo} width={780} onClose={onClose}
      footer={<><div className="grow flex g8 items-center"><span className="subtle">{lang === 'vi' ? 'Đợt' : 'Installment'} {inst.no} · </span><span className="mono" style={{ fontWeight:700, color:'var(--primary)' }}>{vnd(inst.amount)}</span></div>
        <button className="btn" onClick={onClose}>{t('c.close')}</button>
        <button className="btn btn-primary" onClick={doPrint}><i className="bi bi-filetype-pdf"></i>{t('hd.genPdf')}</button></>}>
      <div style={{ marginBottom:8 }} className="flex g8 items-center subtle"><i className="bi bi-eye"></i>{t('hd.preview')}</div>
      <iframe title="preview" srcDoc={html} style={{ width:'100%', height:'56vh', border:'1px solid var(--border)', borderRadius:'var(--r-md)', background:'#fff' }}></iframe>
    </Modal>
  );
}

Object.assign(window, {
  docTienChu, paymentRequestHTML, paymentRequestTemplate, printDocument, PaymentRequestModal,
  SWO_COMPANY, docCompany, docFontStack, DEFAULT_PAYREQ_TITLE, DEFAULT_PAYREQ_INTRO, DEFAULT_PAYREQ_NOTE, DEFAULT_PAYREQ_SIGN,
  // shared doc helpers (header/footer · biến · Word · preview)
  docFlag, docEsc, docDateVN, docVars, applyDocVars, renderDocText, docHeaderMotto, docHeaderCompany, docHeadBar, docHeaderNote, docFooter,
  DOC_HDRNOTE_CSS, DEFAULT_DOC_MOTTO_TITLE, DEFAULT_DOC_MOTTO_SUB, DEFAULT_DOC_FOOTER,
  DOC_VAR_HELP, DOC_VAR_HELP_PAYREQ, DOC_VAR_HELP_CONTRACT,
  DocPreviewModal,
  DOC_SAMPLE_CONTRACT, DOC_SAMPLE_INST, DOC_SAMPLE_PARTY, DOC_SAMPLE_REP,
  // template v2 (2 cột) — model + helper + editor components
  DOC_TPL_CODES, DOC_TPL_CODES_CONTRACT, DOC_TPL_CODES_PAYREQ, DOC_HEAD2_CSS,
  docLogoHtml, renderRichVars, docHeader2, docDefaultHeader, docPlainToRich, docTpl, docTplStyle,
  TplRich, TplCodeList, TplConfigPanel, TplHeaderEditor, TplTermsEditor, TplBuilder, TplSec, DocTemplateEditor,
});
