/* global React, Icon, useToast, Modal */
// ============================================================
// dotmatrix.jsx — ระบบรายงานสำหรับเครื่องพิมพ์ Dot Matrix
// แยกจากรายงาน A4 ปกติโดยสิ้นเชิง · ทำงานเฉพาะแอป Windows
// กดพิมพ์ = ส่งเข้าเครื่องทันที ไม่มีหน้าต่างตั้งค่า (silent print)
//
// หลักการออกแบบสำหรับ dot matrix:
//   - ขาว-ดำล้วน ห้ามพื้นทึบ (เปลืองผ้าหมึก + พิมพ์ช้ามาก)
//   - เส้นตรงบางๆ เท่านั้น · เน้นด้วยตัวหนา ไม่ใช่สี
//   - ตัวอักษรใหญ่พอ (14-15px) อ่านออกที่ 180 dpi
//   - หัวตารางซ้ำทุกแผ่น (กระดาษต่อเนื่อง)
// ============================================================
const { useState: useStateDM, useEffect: useEffectDM } = React;

// ---------- ขนาดกระดาษ (นิ้ว) ----------
// padH ของกระดาษต่อเนื่อง = 0.63" (16 มม.) — เผื่อแถบหนามเตย 0.5" (13 มม.)
// แล้วเว้นจากรอยปรุเข้ามาอีก ~3 มม. ไม่ให้ตัวอักษรชนรอยฉีก
const DM_PAPERS = {
  "9x5.5": { key: "9x5.5", label: "9 × 5.5 นิ้ว — ต่อเนื่อง ครึ่งแผ่น", short: "9 × 5.5 นิ้ว (ต่อเนื่อง ครึ่งแผ่น)", w: 9, h: 5.5, padV: 0.26, padH: 0.63, tractor: true },
  "9x11":  { key: "9x11",  label: "9 × 11 นิ้ว — ต่อเนื่อง เต็มแผ่น",  short: "9 × 11 นิ้ว (ต่อเนื่อง เต็มแผ่น)", w: 9, h: 11,  padV: 0.35, padH: 0.63, tractor: true },
  "a4":    { key: "a4",    label: "A4 — 210 × 297 มม. (กระดาษธรรมดา)", short: "A4 (กระดาษธรรมดา)", w: 8.27, h: 11.69, padV: 0.45, padH: 0.45 },
  "a5l":   { key: "a5l",   label: "A5 แนวนอน — 210 × 148 มม.", short: "A5 แนวนอน (210 × 148 มม.)", w: 8.27, h: 5.83, padV: 0.30, padH: 0.38 },
  "a5":    { key: "a5",    label: "A5 แนวตั้ง — 148 × 210 มม.", short: "A5 แนวตั้ง (148 × 210 มม.)", w: 5.83, h: 8.27, padV: 0.38, padH: 0.38 },
};
const MM_PER_IN = 25.4;

// ---------- ความคมชัดของตัวอักษร ----------
// หัวเข็มพิมพ์ด้วยจุด ตัวเล็กๆ ที่มีสระบน-ล่างและวรรณยุกต์จะเละก่อนเพื่อน
// ทั้ง 3 ค่านี้ปรับได้จากหน้าตั้งค่า เพราะเครื่อง 9 เข็มกับ 24 เข็มให้ผลต่างกันมาก
const DM_FONTS = {
  sarabun: {
    key: "sarabun", label: "TH Sarabun New — ตัวแคบ ใส่ข้อมูลได้เยอะ",
    stack: "'TH Sarabun New','Sarabun','Tahoma',sans-serif", web: true,
  },
  tahoma: {
    key: "tahoma", label: "Tahoma — เส้นหนากว่า ชัดกว่าบนหัวเข็ม (กินที่กว้างกว่า)",
    stack: "'Tahoma','TH Sarabun New',sans-serif", web: false,
  },
};
// ใบสรุปรายหลังใส่ได้กี่งวดต่อแผ่น ที่ขนาดตัวอักษร 100% / 115% / 130% / 150%
// วัดจริงด้วยเครื่องมือ layout ตัวเดียวกับที่ใช้พิมพ์ — ไม่ใช่ค่าประมาณ
const DM_CAPACITY = {
  "9x5.5": [7, 4, 3, 1],
  "a5l":   [8, 5, 3, 2],
  "a5":    [17, 13, 10, 7],
  "9x11":  ["20+", "20+", "20+", 16],
  "a4":    ["20+", "20+", "20+", 17],
};
// ชุดค่าสำเร็จรูปของโหมดกราฟิก — กดทีเดียวได้ครบ ไม่ต้องไล่ปรับทีละช่อง
const DM_PRESETS = [
  {
    key: "sharp", name: "ชัดที่สุด",
    hint: "Tahoma + ใหญ่ขึ้น 15% + เข้มพิเศษ — เริ่มจากอันนี้ถ้าตัวเล็กอ่านไม่ชัด",
    v: { fontKey: "tahoma", fontScale: 1.15, boldAll: true },
  },
  {
    key: "sharpest", name: "ชัดที่สุด (ตัวใหญ่)",
    hint: "เหมือนข้างบนแต่ใหญ่ขึ้น 30% — ชัดสุดแต่ใส่งวดได้น้อยลงมาก",
    v: { fontKey: "tahoma", fontScale: 1.3, boldAll: true },
  },
  {
    key: "default", name: "ค่าเริ่มต้น",
    hint: "TH Sarabun New ขนาดปกติ ไม่เข้มพิเศษ — ใส่ข้อมูลได้เยอะที่สุด",
    v: { fontKey: "sarabun", fontScale: 1, boldAll: false },
  },
];

const DM_SCALES = [
  { v: 1,    label: "ปกติ" },
  { v: 1.15, label: "ใหญ่ขึ้น 15%" },
  { v: 1.3,  label: "ใหญ่ขึ้น 30%" },
  { v: 1.5,  label: "ใหญ่ขึ้น 50%" },
];
const DM_PAPER_LIST = Object.values(DM_PAPERS);

// ---------- ประเภทรายงาน + กระดาษเริ่มต้น ----------
const DM_REPORTS = {
  house:    { key: "house",    name: "รายงานรายหลัง",       defaultPaper: "9x5.5" },
  contract: { key: "contract", name: "รายงานรายสัญญา",      defaultPaper: "9x11" },
  ledger:   { key: "ledger",   name: "ประวัติการเบิกจ่าย",  defaultPaper: "9x11" },
};

// ---------- ตั้งค่า (เก็บใน localStorage ต่อเครื่อง) ----------
const DM_KEY = "jscontract_dotmatrix";

function getDmSettings() {
  const base = {
    printerName: "",
    papers: { house: "9x5.5", contract: "9x11", ledger: "9x11" },
    copies: 1,
    offsetXmm: 0,   // ปรับขอบซ้าย-ขวา (+ เข้าใน / − ออกนอก) หน่วยมิลลิเมตร
    offsetYmm: 0,   // ปรับขอบบน-ล่าง
    fontScale: 1,       // ตัวคูณขนาดตัวอักษรทั้งฉบับ
    boldAll: false,     // เดินเส้นหนาขึ้นทั้งฉบับ — เข้มขึ้น แต่หัวเข็มตีซ้ำจึงช้าลง
    fontKey: "sarabun", // แบบอักษรที่ใช้
    // ---- โหมดการพิมพ์ ----
    // graphic = ส่งเป็นรูปภาพ (จัดหน้าสวย มีเส้นกรอบ แต่ตัวเล็กเบลอ)
    // escp    = ส่งไบต์ให้เครื่องใช้ฟอนต์ในตัวเอง (คมกริบ เร็วมาก แต่ตัวอักษรกว้างเท่ากันหมด)
    engine: "graphic",
    escpCpi: 10,         // ตัวอักษรต่อนิ้ว
    escpQuality: "lq",   // lq = คม · draft = เร็ว
    escpTable: "",       // ตารางอักขระไทย ("" = ใช้ค่าที่ตั้งไว้ที่เครื่อง)
    escpLineEnd: "crlf", // ท้ายบรรทัด
    escpIndent: 0,       // เยื้องซ้ายกี่ตัวอักษร
    escpMarks: "none",   // เครื่องเลื่อนหัวพิมพ์ให้สระ/วรรณยุกต์ไหม (ดูจากหน้าทดสอบแนวคอลัมน์)
    escpThaiCode: "",    // รหัสตารางไทยที่สั่งด้วย ESC ( t ("" = ไม่สั่ง ใช้ ESC t ตามปกติ)
  };
  try {
    const saved = JSON.parse(localStorage.getItem(DM_KEY)) || {};
    return { ...base, ...saved, papers: { ...base.papers, ...(saved.papers || {}) } };
  } catch (e) {
    return base;
  }
}
function saveDmSettings(s) {
  localStorage.setItem(DM_KEY, JSON.stringify(s));
  // แจ้งปุ่มพิมพ์ทุกจุดให้อัปเดตชื่อกระดาษที่แสดงทันที
  try { window.dispatchEvent(new CustomEvent("dm-settings-changed")); } catch (e) {}
}
function dmPaperFor(reportKey) {
  const s = getDmSettings();
  return DM_PAPERS[s.papers[reportKey]] || DM_PAPERS[DM_REPORTS[reportKey].defaultPaper];
}
// ระบบพร้อมใช้เมื่ออยู่ในแอป Windows เท่านั้น
function dmAvailable() {
  if (!window.Desktop || typeof window.Desktop.printSilent !== "function") return false;
  // preload รุ่นใหม่ส่ง isWindows มาให้ · รุ่นเก่าไม่มี → เดาจาก userAgent
  if (typeof window.Desktop.isWindows === "boolean") return window.Desktop.isWindows;
  return /Windows/i.test(navigator.userAgent || "");
}
// อยู่ในแอปแล้วแต่ยังไม่มีระบบพิมพ์ = แอปเวอร์ชันเก่า ต้องอัปเดตก่อน
function dmNeedsAppUpdate() {
  return !!(window.Desktop && typeof window.Desktop.printSilent !== "function");
}

// เลือกใช้ ESC/P เมื่อผู้ใช้ตั้งไว้ และแอปรองรับจริง
function dmUseEscp() {
  return getDmSettings().engine === "escp"
    && typeof window.escpAvailable === "function" && window.escpAvailable();
}
// ตั้งไว้ว่าใช้ ESC/P แต่แอปเก่าเกินไป — ต้องบอกให้ชัด ไม่ใช่เงียบๆ กลับไปโหมดเดิม
function dmEscpBlocked() {
  return getDmSettings().engine === "escp" && !dmUseEscp();
}

// ---------- helpers ----------
function dmEsc(s) {
  return String(s == null ? "" : s).replace(/[&<>"']/g, (m) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[m]));
}
function dmFmt(n) {
  return (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function dmDate(d) { return d ? String(d) : "-"; }
function dmNow() {
  const d = new Date(), p = (n) => String(n).padStart(2, "0");
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
}
function dmStatus(price, paid) {
  price = Number(price) || 0; paid = Number(paid) || 0;
  // ราคา 0 = ยังไม่ได้ตั้งมูลค่า ไม่ใช่ "เบิกครบ" (สัญญา/บ้านที่เพิ่งสร้างเป็น 0/0)
  if (price <= 0) return { text: paid > 0 ? "*เบิกเกิน*" : "ยังไม่ตั้งมูลค่า", over: paid > 0 };
  if (paid > price) return { text: "*เบิกเกิน*", over: true };
  if (paid >= price) return { text: "เบิกครบ" };
  return { text: "ค้างจ่าย" };
}

// ============================================================
// โครงเอกสาร + CSS กลางสำหรับ dot matrix
// ============================================================
// คำนวณขอบจริง = ค่าเริ่มต้นของกระดาษ + ค่าปรับละเอียดจากหน้าตั้งค่า (มม.)
function dmPads(paper, offsets) {
  const ox = ((offsets && offsets.x) || 0) / MM_PER_IN;
  const oy = ((offsets && offsets.y) || 0) / MM_PER_IN;
  return {
    padH: Math.max(0.05, +(paper.padH + ox).toFixed(3)),
    padV: Math.max(0.05, +(paper.padV + oy).toFixed(3)),
  };
}

// ทุกขนาดตัวอักษรคูณด้วย var(--k) เพื่อให้ขยายทั้งฉบับพร้อมกันโดยสัดส่วนไม่เพี้ยน
// (ใช้ em ไม่ได้ เพราะ th ซ้อนใน table จะคูณทบกันเอง)
function dmShell(paper, title, body, opt) {
  opt = opt || {};
  const pad = dmPads(paper, opt);
  const k = Math.max(0.8, Math.min(1.6, Number(opt.k) || 1));
  const font = DM_FONTS[opt.fontKey] || DM_FONTS.sarabun;
  // ฟอนต์ระบบไม่ต้องโหลดจากเน็ต — เร็วกว่าและไม่มีจังหวะที่ฟอนต์ยังมาไม่ถึง
  const fontLink = font.web
    ? `<link rel="stylesheet" href="https://fonts.cdnfonts.com/css/th-sarabun-new">`
    : "";
  const fw = opt.bold ? 600 : 400;
  const fwB = opt.bold ? 700 : 700;
  // -webkit-text-stroke เดินเส้นให้อ้วนขึ้นจริงในตอน rasterize
  // ได้ผลกว่า font-weight เพราะ TH Sarabun New เว็บฟอนต์ไม่มีน้ำหนักหนาจริง
  const stroke = opt.bold ? "0.2px" : "0";
  const strokeB = opt.bold ? "0.3px" : "0";
  return `<!DOCTYPE html><html lang="th"><head><meta charset="UTF-8"><title>${dmEsc(title)}</title>
${fontLink}
<style>
@page { size: ${paper.w}in ${paper.h}in; margin: 0; }
:root{ --k:${k}; --fw:${fw}; --fwb:${fwB}; --st:${stroke}; --stb:${strokeB} }
*{margin:0;padding:0;box-sizing:border-box}
body{
  font-family:${font.stack};
  font-size:calc(15.5px * var(--k)); line-height:1.38; color:#000; background:#fff;
  font-weight:var(--fw); -webkit-text-stroke:var(--st) currentColor;
  padding:${pad.padV}in ${pad.padH}in;
  width:${paper.w}in;
  -webkit-font-smoothing:none;
}
b,.b{font-weight:var(--fwb);-webkit-text-stroke:var(--stb) currentColor}
.r{text-align:right}.c{text-align:center}
.num{font-variant-numeric:tabular-nums}

/* หัวเอกสาร */
.dm-head{display:flex;justify-content:space-between;align-items:flex-end;
  border-bottom:2px solid #000;padding-bottom:2px;margin-bottom:5px}
.dm-sys{font-size:calc(13px * var(--k));letter-spacing:1px}
.dm-title{font-size:calc(19px * var(--k));font-weight:var(--fwb);line-height:1.15;
  -webkit-text-stroke:var(--stb) currentColor}
.dm-ref{font-size:calc(13px * var(--k));text-align:right;line-height:1.4}

/* บล็อกข้อมูล key-value 2 คอลัมน์ */
.dm-kv{display:flex;flex-wrap:wrap;margin-bottom:3px}
.dm-kv .cell{width:50%;font-size:calc(15px * var(--k));padding:0.5px 0}
.dm-kv .k{display:inline-block;min-width:calc(74px * var(--k))}
.dm-kv .w100{width:100%}

/* แถบยอดเงิน */
.dm-money{display:flex;justify-content:space-between;font-size:calc(15.5px * var(--k));
  border-top:1px solid #000;border-bottom:2px solid #000;padding:3px 0;margin:4px 0 5px}
.dm-money .m{white-space:nowrap}
.dm-money .m i{font-style:normal;font-weight:var(--fw);-webkit-text-stroke:var(--st) currentColor}
.dm-money .m b{font-variant-numeric:tabular-nums}

/* ตาราง */
table.dm{width:100%;border-collapse:collapse;font-size:calc(15px * var(--k))}
table.dm thead{display:table-header-group}
table.dm th{border-top:1px solid #000;border-bottom:1px solid #000;
  padding:2px 4px;text-align:left;font-weight:var(--fwb);font-size:calc(14px * var(--k));
  -webkit-text-stroke:var(--stb) currentColor}
table.dm td{padding:1.5px 4px;vertical-align:top}
table.dm td.num,table.dm th.num{font-variant-numeric:tabular-nums}
table.dm tr.unit td{border-top:1px dotted #000}
table.dm tr.unit:first-child td{border-top:0}
table.dm tr.sub td{font-size:calc(14px * var(--k));padding:0.5px 4px 0.5px 16px}
table.dm tfoot td{border-top:1px solid #000;border-bottom:2px solid #000;
  font-weight:var(--fwb);padding:3px 4px;font-variant-numeric:tabular-nums;
  -webkit-text-stroke:var(--stb) currentColor}
tr{break-inside:avoid}
tbody.grp{break-inside:avoid}

/* ท้ายเอกสาร */
.dm-sum{text-align:right;font-size:calc(14px * var(--k));margin-top:2px}
/* คำอ่านจำนวนเงินในแถวรวม — ไม่ต้องหนาตาม tfoot */
.baht{font-weight:var(--fw);-webkit-text-stroke:var(--st) currentColor}
.dm-sig{display:flex;gap:22px;margin-top:12px;font-size:calc(14.5px * var(--k))}
.dm-sig .box{flex:1;text-align:center}
.dm-sig .line{margin-bottom:2px}
.dm-foot{margin-top:8px;border-top:1px solid #000;padding-top:2px;
  display:flex;justify-content:space-between;font-size:calc(13px * var(--k))}
.dm-note{font-size:calc(14px * var(--k));margin-top:3px}
.dm-empty{text-align:center;font-size:calc(14.5px * var(--k));padding:10px 0;font-style:italic}
.dm-sample{border:1px solid #000;padding:3px 5px;margin:4px 0 6px}
.dm-sample .row{display:flex;align-items:baseline;gap:8px;padding:1px 0}
.dm-sample .tag{width:calc(64px * var(--k));font-size:calc(12px * var(--k));flex:none}
</style></head><body>${body}</body></html>`;
}

function dmHead(title, refLines) {
  return `<div class="dm-head">
    <div>
      <div class="dm-sys">JS CONTRACT SYSTEM</div>
      <div class="dm-title">${dmEsc(title)}</div>
    </div>
    <div class="dm-ref">${refLines.map((l) => dmEsc(l)).join("<br>")}</div>
  </div>`;
}
function dmSignRow(roles) {
  return `<div class="dm-sig">${roles.map((r) => `
    <div class="box">
      <div class="line">.................................................</div>
      <div>( ${dmEsc(r)} )</div>
      <div>วันที่ ......../......../........</div>
    </div>`).join("")}</div>`;
}
function dmFoot(left) {
  return `<div class="dm-foot"><span>${dmEsc(left)}</span><span>พิมพ์ ${dmNow()}</span></div>`;
}

// ============================================================
// 1) ใบสรุปการเบิกจ่ายรายหลัง — 9 × 5.5 นิ้ว
// ============================================================
function dmHouseSlipHTML({ project, contract, house, unitLbl }) {
  const price = house.price || 0, paid = house.paid_total || 0, remain = price - paid;
  const st = dmStatus(price, paid);
  const pays = (house.payments || []).slice()
    .sort((a, b) => (a.date > b.date ? 1 : a.date < b.date ? -1 : 0));
  const sum = pays.reduce((s, p) => s + (p.amount || 0), 0);

  const rows = pays.length
    ? pays.map((p, i) => `<tr>
        <td class="c">${i + 1}</td>
        <td>${dmEsc(dmDate(p.date))}</td>
        <td>${dmEsc(p.inv || "-")}</td>
        <td class="r num">${dmFmt(p.amount)}</td>
        <td>${dmEsc(p.notes || "")}</td>
      </tr>`).join("")
    : `<tr><td colspan="5" class="dm-empty">— ยังไม่มีรายการเบิกจ่าย —</td></tr>`;

  const body =
    dmHead(`ใบสรุปการเบิกจ่าย${unitLbl.singular}`, [
      `${unitLbl.code} ${house.house_code || "-"}`,
      `สัญญา ${contract.contract_number || "-"}`,
    ]) +
    `<div class="dm-kv">
      <div class="cell"><span class="k">โครงการ</span> <b>${dmEsc(project.name || "-")}</b></div>
      <div class="cell"><span class="k">สัญญาเลขที่</span> <b>${dmEsc(contract.contract_number || "-")}</b></div>
      <div class="cell"><span class="k">${dmEsc(unitLbl.code)}</span> <b>${dmEsc(house.house_code || "-")}</b></div>
      <div class="cell"><span class="k">วันที่สัญญา</span> ${dmEsc(dmDate(contract.contract_date))}</div>
      ${house.notes ? `<div class="cell w100"><span class="k">หมายเหตุ</span> ${dmEsc(house.notes)}</div>` : ""}
    </div>
    <div class="dm-money">
      <span class="m"><i>ราคา</i> <b>${dmFmt(price)}</b></span>
      <span class="m"><i>เบิกแล้ว</i> <b>${dmFmt(paid)}</b></span>
      <span class="m"><i>คงเหลือ</i> <b>${dmFmt(Math.abs(remain))}</b></span>
      <span class="m"><b>${dmEsc(st.text)}</b></span>
    </div>
    <table class="dm">
      <thead><tr>
        <th class="c" style="width:7%">งวด</th>
        <th style="width:18%">วันที่</th>
        <th style="width:25%">เลขที่ใบกำกับ</th>
        <th class="r num" style="width:20%">จำนวนเงิน</th>
        <th style="width:30%">หมายเหตุ</th>
      </tr></thead>
      <tbody>${rows}</tbody>
      <tfoot><tr>
        <td colspan="3">รวม ${pays.length} งวด <span class="baht">(${dmEsc(window.bahtText ? window.bahtText(sum) : "")})</span></td>
        <td class="r num">${dmFmt(sum)}</td>
        <td></td>
      </tr></tfoot>
    </table>` +
    dmSignRow(["ผู้จัดทำ", "ผู้อนุมัติ"]) +
    dmFoot(`${project.name || ""} · ${contract.contract_number || ""}`);

  return body;
}

// ============================================================
// 2) รายงานสรุปสัญญา — 9 × 11 นิ้ว
// ============================================================
function dmContractSheetHTML({ project, contract, unitLbl }) {
  const houses = (contract.houses || []).slice()
    .sort((a, b) => (a.house_code || "").localeCompare(b.house_code || "", undefined, { numeric: true, sensitivity: "base" }));

  let totPrice = 0, totPaid = 0, totPays = 0;
  const groups = houses.length
    ? houses.map((h) => {
        const price = h.price || 0, paid = h.paid_total || 0, remain = price - paid;
        // comparator ต้องคืน 0 เมื่อเท่ากัน — เดิมคืน -1 ทำให้ลำดับไม่แน่นอน
        // และเลข "งวดที่ N" บนเอกสารที่เซ็นแล้วสลับกันได้ระหว่างพิมพ์สองครั้ง
        const pays = (h.payments || []).slice().sort((a, b) => (a.date > b.date ? 1 : a.date < b.date ? -1 : 0));
        const st = dmStatus(price, paid);
        totPrice += price; totPaid += paid; totPays += pays.length;

        const subs = pays.length
          ? pays.map((p, i) => `<tr class="sub">
              <td colspan="2">งวด ${i + 1}  ${dmEsc(dmDate(p.date))}</td>
              <td>${dmEsc(p.inv || "-")}</td>
              <td class="r num">${dmFmt(p.amount)}</td>
              <td colspan="2">${dmEsc(p.notes || "")}</td>
            </tr>`).join("")
          : `<tr class="sub"><td colspan="6">— ยังไม่มีการเบิกจ่าย —</td></tr>`;

        return `<tbody class="grp">
          <tr class="unit">
            <td class="b">${dmEsc(h.house_code || "-")}</td>
            <td class="r num">${dmFmt(price)}</td>
            <td class="r num">${dmFmt(paid)}</td>
            <td class="r num">${dmFmt(Math.abs(remain))}</td>
            <td class="c">${pays.length}</td>
            <td>${dmEsc(st.text)}</td>
          </tr>
          ${subs}
        </tbody>`;
      }).join("")
    : `<tbody><tr><td colspan="6" class="dm-empty">— ยังไม่มี${dmEsc(unitLbl.singular)}ในสัญญานี้ —</td></tr></tbody>`;

  const cVal = contract.value_total || 0, cPaid = contract.paid_total || 0;
  const pct = cVal > 0 ? ((cPaid / cVal) * 100).toFixed(1) : "0.0";

  const body =
    dmHead("รายงานสรุปสัญญา", [
      `สัญญา ${contract.contract_number || "-"}`,
      dmDate(contract.contract_date),
    ]) +
    `<div class="dm-kv">
      <div class="cell"><span class="k">สัญญาเลขที่</span> <b>${dmEsc(contract.contract_number || "-")}</b></div>
      <div class="cell"><span class="k">วันที่สัญญา</span> ${dmEsc(dmDate(contract.contract_date))}</div>
      <div class="cell"><span class="k">โครงการ</span> <b>${dmEsc(project.name || "-")}</b></div>
      <div class="cell"><span class="k">ลูกค้า</span> ${dmEsc(project.client || "-")}</div>
      <div class="cell w100"><span class="k">รายละเอียด</span> ${dmEsc(contract.description || "-")}</div>
    </div>
    <div class="dm-money">
      <span class="m"><i>มูลค่าสัญญา</i> <b>${dmFmt(cVal)}</b></span>
      <span class="m"><i>เบิกแล้ว</i> <b>${dmFmt(cPaid)}</b></span>
      <span class="m"><i>คงเหลือ</i> <b>${dmFmt(cVal - cPaid)}</b></span>
      <span class="m"><i>คืบหน้า</i> <b>${pct}%</b></span>
    </div>
    <table class="dm">
      <thead><tr>
        <th style="width:16%">${dmEsc(unitLbl.code)}</th>
        <th class="r num" style="width:17%">ราคา</th>
        <th class="r num" style="width:17%">เบิกแล้ว</th>
        <th class="r num" style="width:17%">คงเหลือ</th>
        <th class="c" style="width:8%">งวด</th>
        <th style="width:25%">สถานะ</th>
      </tr></thead>
      ${groups}
      <tfoot><tr>
        <td>รวม ${houses.length} ${dmEsc(unitLbl.counter)}</td>
        <td class="r num">${dmFmt(totPrice)}</td>
        <td class="r num">${dmFmt(totPaid)}</td>
        <td class="r num">${dmFmt(totPrice - totPaid)}</td>
        <td class="c">${totPays}</td>
        <td></td>
      </tr></tfoot>
    </table>
    <div class="dm-sum">ยอดเบิกแล้วเป็นตัวอักษร: (${dmEsc(window.bahtText ? window.bahtText(totPaid) : "")})</div>` +
    dmSignRow(["ผู้จัดทำ", "ผู้ตรวจสอบ", "ผู้อนุมัติ"]) +
    dmFoot(`${project.name || ""} · ${contract.contract_number || ""}`);

  return body;
}

// ============================================================
// 3) รายงานประวัติการเบิกจ่าย — 9 × 11 นิ้ว
// ============================================================
function dmLedgerHTML({ rows, scopeLabel }) {
  const total = rows.reduce((s, r) => s + (r.amount || 0), 0);

  const trs = rows.length
    ? rows.map((r, i) => `<tr>
        <td class="c">${i + 1}</td>
        <td>${dmEsc(dmDate(r.date))}</td>
        <td>${dmEsc(r.project)}</td>
        <td>${dmEsc(r.contract)}</td>
        <td>${dmEsc(r.unit)}</td>
        <td class="c">${r.no}</td>
        <td>${dmEsc(r.inv || "-")}</td>
        <td class="r num">${dmFmt(r.amount)}</td>
      </tr>`).join("")
    : `<tr><td colspan="8" class="dm-empty">— ยังไม่มีรายการเบิกจ่าย —</td></tr>`;

  const body =
    dmHead("รายงานประวัติการเบิกจ่าย", [scopeLabel || "ทุกโครงการ", `${rows.length} รายการ`]) +
    `<div class="dm-money">
      <span class="m"><i>ขอบเขต</i> <b>${dmEsc(scopeLabel || "ทุกโครงการ")}</b></span>
      <span class="m"><i>จำนวน</i> <b>${rows.length} รายการ</b></span>
      <span class="m"><i>ยอดรวม</i> <b>${dmFmt(total)}</b></span>
    </div>
    <table class="dm">
      <thead><tr>
        <th class="c" style="width:5%">ที่</th>
        <th style="width:12%">วันที่</th>
        <th style="width:20%">โครงการ</th>
        <th style="width:14%">สัญญา</th>
        <th style="width:10%">หน่วย</th>
        <th class="c" style="width:6%">งวด</th>
        <th style="width:16%">ใบกำกับ</th>
        <th class="r num" style="width:17%">จำนวนเงิน</th>
      </tr></thead>
      <tbody>${trs}</tbody>
      <tfoot><tr>
        <td colspan="7">รวมทั้งสิ้น ${rows.length} รายการ</td>
        <td class="r num">${dmFmt(total)}</td>
      </tr></tfoot>
    </table>
    <div class="dm-sum">(${dmEsc(window.bahtText ? window.bahtText(total) : "")})</div>` +
    dmSignRow(["ผู้จัดทำ", "ผู้ตรวจสอบ", "ผู้อนุมัติ"]) +
    dmFoot(scopeLabel || "ทุกโครงการ");

  return body;
}

// ============================================================
// 4) หน้าทดสอบพิมพ์ / จัดตำแหน่งกระดาษ
// ============================================================
function dmTestPageHTML(paper, offsets) {
  const pad = dmPads(paper, offsets);
  const inches = Math.floor(paper.w - pad.padH * 2);
  let ruler = "";
  for (let i = 0; i < inches; i++) {
    ruler += `<div style="width:1in;border-left:1px solid #000;height:calc(15px * var(--k));font-size:calc(12px * var(--k));padding-left:2px">${i + 1}"</div>`;
  }
  const mmH = (pad.padH * MM_PER_IN).toFixed(1), mmV = (pad.padV * MM_PER_IN).toFixed(1);

  // ตัวอย่างข้อความไทยที่มีสระบน-ล่างและวรรณยุกต์ครบ ในทุกขนาดที่รายงานจริงใช้
  // ใช้ดูว่าขนาดไหนของเครื่องนี้เริ่มอ่านไม่ออก แล้วค่อยไปปรับในหน้าตั้งค่า
  const s = getDmSettings();
  const fontLbl = (DM_FONTS[s.fontKey] || DM_FONTS.sarabun).label.split(" —")[0];
  const SAMPLE = "ผู้รับเหมาเบิกงวดที่ ๙ ปั๊มน้ำ ญี่ปุ่น 0123456789";
  const sizeSamples =
    `<div class="dm-sample">
      <div style="font-size:calc(13px * var(--k));margin-bottom:2px">
        <b>เทียบขนาดตัวอักษร</b> — ฟอนต์ ${dmEsc(fontLbl)} · ขนาด ${Math.round(s.fontScale * 100)}% · ${s.boldAll ? "เข้มพิเศษ" : "ความหนาปกติ"}
        <br>ดูว่าบรรทัดไหนเริ่มอ่านไม่ออก แล้วไปตั้งค่าให้ขนาดเล็กสุดที่ใช้อยู่ใหญ่กว่านั้น
      </div>
      ${[
        ["13px", "ท้ายเอกสาร"],
        ["14px", "หัวตาราง"],
        ["15px", "ในตาราง"],
        ["15.5px", "ตัวหลัก"],
      ].map(([px, tag]) => `<div class="row">
        <span class="tag">${tag}</span>
        <span style="font-size:calc(${px} * var(--k))">${dmEsc(SAMPLE)}</span>
      </div>`).join("")}
    </div>`;
  const body =
    dmHead("ทดสอบการพิมพ์ Dot Matrix", [paper.label, dmNow()]) +
    `<div class="dm-kv">
      <div class="cell"><span class="k">ขนาดกระดาษ</span> <b>${paper.w} × ${paper.h} นิ้ว</b></div>
      <div class="cell"><span class="k">ขอบที่ใช้</span> ซ้าย-ขวา <b>${mmH} มม.</b> · บน-ล่าง <b>${mmV} มม.</b></div>
      ${paper.tractor ? `<div class="cell w100"><span class="k">รอยปรุ</span> แถบหนามเตยกว้าง ~13 มม. — ข้อความควรเริ่มถัดจากรอยปรุเข้ามา ${(pad.padH * MM_PER_IN - 13).toFixed(1)} มม.</div>` : ""}
    </div>
    <div style="border-left:1px solid #000;border-right:1px solid #000;padding:2px 4px;margin-bottom:5px;font-size:calc(14px * var(--k))">
      ↑ เส้นแนวตั้งสองข้างนี้คือขอบซ้าย-ขวาของเนื้อหา — ต้องอยู่ถัดจากรอยปรุเข้ามา ไม่ทับแถบหนามเตย
    </div>
    ${sizeSamples}
    <div class="dm-money">
      <span class="m"><i>ตัวเลข</i> <b>1,234,567.89</b></span>
      <span class="m"><i>ไทย</i> <b>กขคง ฐฒณฤๅ ไม้เอก่ ไม้โท้</b></span>
      <span class="m"><i>อังกฤษ</i> <b>ABC abc 0123</b></span>
    </div>
    <div style="font-size:calc(14px * var(--k));margin-bottom:2px">ไม้บรรทัดตรวจสเกล — วัดด้วยไม้บรรทัดจริง แต่ละช่องต้องกว้าง 1 นิ้วพอดี</div>
    <div style="display:flex;border-right:1px solid #000;margin-bottom:6px">${ruler}</div>
    <table class="dm">
      <thead><tr>
        <th style="width:20%">คอลัมน์ซ้าย</th>
        <th style="width:30%">คอลัมน์กลาง</th>
        <th class="r num" style="width:25%">ตัวเลขชิดขวา</th>
        <th style="width:25%">หมายเหตุ</th>
      </tr></thead>
      <tbody>
        <tr><td>ทดสอบแถวที่ 1</td><td>ตัวอย่างข้อความไทย</td><td class="r num">123,456.00</td><td>ปกติ</td></tr>
        <tr><td>ทดสอบแถวที่ 2</td><td>ABCDEFG 1234567</td><td class="r num">7,890.50</td><td>ปกติ</td></tr>
      </tbody>
      <tfoot><tr><td colspan="2">รวม</td><td class="r num">131,346.50</td><td></td></tr></tfoot>
    </table>
    <div class="dm-note"><b>ตรวจสอบ:</b> ข้อความไทยครบถ้วน · ไม่มีตัวอักษรตกขอบ · ไม้บรรทัดกว้าง 1 นิ้วต่อช่อง · กระดาษไม่เลื่อนหน้าเกิน</div>` +
    dmFoot("หน้าทดสอบ — ไม่ใช่เอกสารจริง");
  return body;
}

// ============================================================
// ส่งงานเข้าเครื่องพิมพ์
// ============================================================
async function dmPrint({ reportKey, paperKey, title, body, copies }) {
  if (!dmAvailable()) {
    return { ok: false, error: "ระบบพิมพ์ Dot Matrix ใช้ได้เฉพาะในแอปบน Windows" };
  }
  const s = getDmSettings();
  const paper = paperKey ? DM_PAPERS[paperKey] : dmPaperFor(reportKey);
  const html = dmShell(paper, title, body, {
    x: s.offsetXmm, y: s.offsetYmm,
    k: s.fontScale, bold: s.boldAll, fontKey: s.fontKey,
  });
  return window.Desktop.printSilent({
    html,
    deviceName: s.printerName || undefined,
    widthIn: paper.w,
    heightIn: paper.h,
    copies: copies || s.copies || 1,
  });
}

// ---------- ตัวสร้างรายงาน (โหลดข้อมูลสด → สร้าง body → พิมพ์) ----------
async function dmPrintHouse(projectId, contractId, houseId) {
  const scope = { type: "house", projectId, contractId, houseId };
  await window.loadScopedPayments(scope);
  const projects = window.buildHierarchicalData(scope);
  const p = projects[0], c = p && p.contracts[0], h = c && c.houses[0];
  if (!h) return { ok: false, error: "ไม่พบข้อมูลหน่วยที่เลือก" };
  const unitLbl = window.unitLabel(p.unit_type);
  const title = `ใบสรุปการเบิกจ่าย ${h.house_code}`;
  if (dmEscpBlocked()) return { ok: false, error: ESCP_NEED_UPDATE };
  if (dmUseEscp()) {
    return window.escpPrint({
      reportKey: "house", title,
      build: (doc) => window.escpHouseSlip(doc, { project: p, contract: c, house: h, unitLbl }),
    });
  }
  return dmPrint({
    reportKey: "house", title,
    body: dmHouseSlipHTML({ project: p, contract: c, house: h, unitLbl }),
  });
}

async function dmPrintContract(projectId, contractId) {
  const scope = { type: "contract", projectId, contractId };
  await window.loadScopedPayments(scope);
  const projects = window.buildHierarchicalData(scope);
  const p = projects[0], c = p && p.contracts[0];
  if (!c) return { ok: false, error: "ไม่พบข้อมูลสัญญาที่เลือก" };
  const unitLbl = window.unitLabel(p.unit_type);
  const title = `รายงานสัญญา ${c.contract_number}`;
  if (dmEscpBlocked()) return { ok: false, error: ESCP_NEED_UPDATE };
  if (dmUseEscp()) {
    return window.escpPrint({
      reportKey: "contract", title,
      build: (doc) => window.escpContractSheet(doc, { project: p, contract: c, unitLbl }),
    });
  }
  return dmPrint({
    reportKey: "contract", title,
    body: dmContractSheetHTML({ project: p, contract: c, unitLbl }),
  });
}

async function dmPrintLedger(scope, scopeLabel) {
  scope = scope || { type: "all" };
  await window.loadScopedPayments(scope);
  const projects = window.buildHierarchicalData(scope);
  const rows = [];
  for (const p of projects) {
    const lbl = window.unitLabel(p.unit_type);
    for (const c of p.contracts) {
      for (const h of c.houses) {
        (h.payments || []).forEach((py, i) => rows.push({
          date: py.date, project: p.name, contract: c.contract_number,
          unit: `${lbl.singular} ${h.house_code}`, no: i + 1,
          inv: py.inv, amount: py.amount,
        }));
      }
    }
  }
  rows.sort((a, b) => (a.date > b.date ? -1 : a.date < b.date ? 1 : 0));
  const title = "รายงานประวัติการเบิกจ่าย";
  if (dmEscpBlocked()) return { ok: false, error: ESCP_NEED_UPDATE };
  if (dmUseEscp()) {
    return window.escpPrint({
      reportKey: "ledger", title,
      build: (doc) => window.escpLedger(doc, { rows, scopeLabel }),
    });
  }
  return dmPrint({
    reportKey: "ledger", title,
    body: dmLedgerHTML({ rows, scopeLabel }),
  });
}

const ESCP_NEED_UPDATE =
  "ตั้งไว้ให้พิมพ์แบบ ESC/P แต่แอปเวอร์ชันนี้ยังไม่รองรับ — กรุณาอัปเดตแอปก่อน " +
  "(หรือเปลี่ยนกลับเป็นโหมดกราฟิกในหน้าตั้งค่า)";

async function dmPrintTest(paperKey) {
  const paper = DM_PAPERS[paperKey] || DM_PAPERS["9x5.5"];
  const s = getDmSettings();
  if (dmEscpBlocked()) return { ok: false, error: ESCP_NEED_UPDATE };
  if (dmUseEscp()) {
    return window.escpPrint({
      paperKey: paper.key, title: "ทดสอบการพิมพ์ ESC/P",
      build: (doc, st) => window.escpTestPage(doc, st),
    });
  }
  return dmPrint({
    paperKey: paper.key,
    title: "ทดสอบการพิมพ์",
    body: dmTestPageHTML(paper, { x: s.offsetXmm, y: s.offsetYmm }),
  });
}

// ไล่เปลี่ยนรหัสตารางไทยด้วย ESC ( t — หาโหมดที่เครื่องพิมพ์สระทับตัวให้เอง
async function dmPrintThaiModeProbe(paperKey) {
  if (typeof window.escpThaiModeProbeBytes !== "function") {
    return { ok: false, error: ESCP_NEED_UPDATE };
  }
  const paper = DM_PAPERS[paperKey] || DM_PAPERS["a5l"];
  const bytes = window.escpThaiModeProbeBytes(paper, getDmSettings());
  return window.escpSend(bytes, "หาโหมดไทยของเครื่อง");
}

// พิมพ์ตัวอย่างไทยซ้ำโดยไล่เปลี่ยน ESC t ทีละค่า — ใช้หาว่ารุ่นนี้ตารางไทยอยู่ที่ไหน
async function dmPrintTableProbe(paperKey) {
  if (typeof window.escpTableProbeBytes !== "function") {
    return { ok: false, error: ESCP_NEED_UPDATE };
  }
  const paper = DM_PAPERS[paperKey] || DM_PAPERS["9x5.5"];
  const bytes = window.escpTableProbeBytes(paper, getDmSettings());
  return window.escpSend(bytes, "หาตารางอักขระไทย");
}

// ============================================================
// UI: ปุ่มพิมพ์ dot matrix (โชว์เฉพาะแอป Windows)
// ============================================================
function DmPrintButton({ run, label, reportKey, size = "sm", full }) {
  const [busy, setBusy] = useStateDM(false);
  const [, bump] = useStateDM(0);   // re-render เมื่อผู้ใช้เปลี่ยนกระดาษในหน้าตั้งค่า
  const toast = useToast();

  useEffectDM(() => {
    const onChange = () => bump((n) => n + 1);
    window.addEventListener("dm-settings-changed", onChange);
    return () => window.removeEventListener("dm-settings-changed", onChange);
  }, []);

  if (!dmAvailable()) return null;
  const paper = reportKey ? dmPaperFor(reportKey) : null;

  const click = async (e) => {
    e.stopPropagation();
    if (busy) return;
    setBusy(true);
    try {
      const r = await run();
      if (r && r.ok) toast({ t: "ส่งงานเข้าเครื่องพิมพ์แล้ว", s: "เครื่อง dot matrix จะเริ่มพิมพ์ในไม่กี่วินาที" });
      else toast({ t: "พิมพ์ไม่สำเร็จ", s: (r && r.error) || "ไม่ทราบสาเหตุ" });
    } catch (err) {
      toast({ t: "พิมพ์ไม่สำเร็จ", s: err.message });
    }
    setBusy(false);
  };

  const tip = paper
    ? `พิมพ์เข้าเครื่อง Dot Matrix ทันที — ใช้กระดาษ ${paper.short}`
    : "พิมพ์เข้าเครื่อง Dot Matrix ทันที";

  // แบบเต็มความกว้าง: 2 บรรทัด — ชื่อรายงาน + กระดาษที่ต้องใส่
  if (full) {
    return (
      <button className="btn" type="button" onClick={click} disabled={busy} title={tip}
        style={{ width: "100%", flexDirection: "column", alignItems: "center", gap: 2,
                 padding: "9px 12px", height: "auto", whiteSpace: "normal" }}>
        <span style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 14, fontWeight: 600 }}>
          {busy ? <span className="spinner" /> : <Icon name="print" size={14} />}
          {busy ? "กำลังส่งเข้าเครื่องพิมพ์..." : (label || "พิมพ์ Dot Matrix")}
        </span>
        {paper && !busy && (
          <span style={{ fontSize: 12.5, fontWeight: 400, color: "var(--text-2)" }}>
            ใส่กระดาษ <b style={{ color: "var(--text)" }}>{paper.short}</b>
          </span>
        )}
      </button>
    );
  }

  // แบบอินไลน์: บรรทัดเดียว มีขนาดกระดาษต่อท้าย
  return (
    <button className={`btn ${size}`} type="button" onClick={click} disabled={busy} title={tip}
      style={{ fontSize: 13 }}>
      {busy
        ? <><span className="spinner" /> กำลังส่ง...</>
        : <>
            <Icon name="print" size={13} /> {label || "Dot Matrix"}
            {paper && <span style={{ color: "var(--text-2)", fontWeight: 400 }}>· {paper.w}×{paper.h}"</span>}
          </>}
    </button>
  );
}

// ============================================================
// UI: การ์ดตั้งค่าเครื่องพิมพ์ (หน้าตั้งค่า)
// ============================================================
function DotMatrixSettingsBody() {
  const [settings, setSettings] = useStateDM(getDmSettings());
  const [printers, setPrinters] = useStateDM(null);   // null = กำลังโหลด
  const [testing, setTesting] = useStateDM("");
  // เก็บข้อความดิบของช่องปรับขอบไว้ต่างหาก เพื่อให้พิมพ์ "-" ค้างไว้ได้
  // (input type=number รายงานค่ากลางคัน "-" เป็นสตริงว่าง ทำให้ค่าติดลบพิมพ์ไม่ได้เลย)
  const [rawX, setRawX] = useStateDM(String(getDmSettings().offsetXmm));
  const [rawY, setRawY] = useStateDM(String(getDmSettings().offsetYmm));
  const [rawIndent, setRawIndent] = useStateDM(String(getDmSettings().escpIndent));
  const [rawThai, setRawThai] = useStateDM(String(getDmSettings().escpThaiCode || ""));
  const toast = useToast();
  // แอปเก่ายังไม่มีช่องทางส่งไบต์ดิบ — ต้องเตือนตั้งแต่ตอนเลือก ไม่ใช่ตอนกดพิมพ์
  const escpUnsupported = typeof window.escpAvailable !== "function" || !window.escpAvailable();

  useEffectDM(() => {
    if (!dmAvailable()) { setPrinters([]); return; }
    window.Desktop.listPrinters()
      .then((r) => setPrinters(r && r.ok ? r.printers : []))
      .catch(() => setPrinters([]));
  }, []);

  const update = (patch) => {
    const next = { ...settings, ...patch };
    setSettings(next);
    saveDmSettings(next);
  };
  // แปลงข้อความดิบ → ตัวเลข พร้อมจำกัดช่วง (ยอมให้ "-" ค้างระหว่างพิมพ์)
  const setOffset = (axis, text, setRaw) => {
    const v = text.replace(/[^0-9-]/g, "").replace(/(?!^)-/g, "");
    setRaw(v);
    const n = (v === "" || v === "-") ? 0 : Number(v);
    const clamped = Number.isFinite(n) ? Math.max(-10, Math.min(25, n)) : 0;
    update(axis === "x" ? { offsetXmm: clamped } : { offsetYmm: clamped });
  };
  const setPaper = (reportKey, paperKey) => {
    const next = { ...settings, papers: { ...settings.papers, [reportKey]: paperKey } };
    setSettings(next);
    saveDmSettings(next);
  };

  const doThaiProbe = async (paperKey) => {
    setTesting("thai:" + paperKey);
    try {
      const r = await dmPrintThaiModeProbe(paperKey);
      if (r && r.ok) toast({ t: "ส่งหน้าหาโหมดไทยแล้ว", s: "หาบรรทัดที่สระทับตัวถูกต้อง แล้วกรอกรหัสนั้น" });
      else toast({ t: "พิมพ์ไม่สำเร็จ", s: (r && r.error) || "ไม่ทราบสาเหตุ" });
    } catch (e) {
      toast({ t: "พิมพ์ไม่สำเร็จ", s: e.message });
    }
    setTesting("");
  };

  const doProbe = async (paperKey) => {
    setTesting("probe:" + paperKey);
    try {
      const r = await dmPrintTableProbe(paperKey);
      if (r && r.ok) toast({ t: "ส่งหน้าหาตารางอักขระแล้ว", s: "ดูว่าบรรทัดไหนอ่านออก แล้วเลือกค่านั้น" });
      else toast({ t: "พิมพ์ไม่สำเร็จ", s: (r && r.error) || "ไม่ทราบสาเหตุ" });
    } catch (e) {
      toast({ t: "พิมพ์ไม่สำเร็จ", s: e.message });
    }
    setTesting("");
  };

  const doTest = async (paperKey) => {
    setTesting(paperKey);
    try {
      const r = await dmPrintTest(paperKey);
      if (r && r.ok) toast({ t: "ส่งหน้าทดสอบแล้ว", s: "ตรวจไม้บรรทัดว่ากว้าง 1 นิ้วต่อช่อง" });
      else toast({ t: "พิมพ์ไม่สำเร็จ", s: (r && r.error) || "ไม่ทราบสาเหตุ" });
    } catch (e) {
      toast({ t: "พิมพ์ไม่สำเร็จ", s: e.message });
    }
    setTesting("");
  };

  return (
      <div className="vstack" style={{ gap: 12 }}>
        <div className="field">
          <label>เครื่องพิมพ์ที่ใช้</label>
          {printers === null ? (
            <div className="dim" style={{ fontSize: 13, padding: "8px 0" }}>
              <span className="spinner" style={{ width: 12, height: 12 }}></span> กำลังค้นหาเครื่องพิมพ์...
            </div>
          ) : (
            <select className="select" value={settings.printerName}
              onChange={(e) => update({ printerName: e.target.value })}>
              <option value="">— เครื่องพิมพ์เริ่มต้นของ Windows —</option>
              {printers.map((p) => (
                <option key={p.name} value={p.name}>
                  {p.displayName}{p.isDefault ? " (ค่าเริ่มต้น)" : ""}
                </option>
              ))}
            </select>
          )}
          <div className="dim" style={{ fontSize: 11.5, marginTop: 4 }}>
            กดพิมพ์แล้วระบบส่งเข้าเครื่องนี้ทันที ไม่มีหน้าต่างให้ตั้งค่าอีก
          </div>
        </div>

        <div className="divider"></div>

        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 8 }}>ขนาดกระดาษของแต่ละรายงาน</div>
          <div className="vstack" style={{ gap: 8 }}>
            {Object.values(DM_REPORTS).map((r) => (
              <div key={r.key} className="hstack" style={{ gap: 10 }}>
                <div style={{ width: 150, fontSize: 13 }}>{r.name}</div>
                <select className="select" style={{ flex: 1 }}
                  value={settings.papers[r.key]}
                  onChange={(e) => setPaper(r.key, e.target.value)}>
                  {DM_PAPER_LIST.map((p) => <option key={p.key} value={p.key}>{p.label}</option>)}
                </select>
              </div>
            ))}
          </div>
        </div>

        <div className="divider"></div>

        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 4 }}>วิธีส่งงานเข้าเครื่อง</div>
          <div className="dim" style={{ fontSize: 12, marginBottom: 8 }}>
            ตัวเลือกนี้เปลี่ยนหน้าตาเอกสารทั้งหมด — พิมพ์หน้าทดสอบดูก่อนใช้งานจริง
          </div>
          <div className="vstack" style={{ gap: 6 }}>
            {[
              { v: "graphic", t: "กราฟิก (เดิม)",
                d: "ส่งเป็นรูปภาพให้หัวเข็มยิงจุด — จัดหน้าสวย มีเส้นกรอบและตัวอักษรหลายขนาด แต่ตัวเล็กๆ เบลอ" },
              { v: "escp", t: "ESC/P — ใช้ฟอนต์ในตัวเครื่อง",
                d: "ส่งตัวอักษรให้เครื่องพิมพ์เอง คมกริบและเร็วกว่ามาก แต่ตัวอักษรกว้างเท่ากันหมด ไม่มีเส้นกรอบ "
                   + "— ใช้ได้ต่อเมื่อเครื่องประกอบสระไทยให้เอง ถ้าไม่ประกอบ สระจะแยกออกมาเป็นตัวๆ อ่านยาก "
                   + "(เช่น Epson PLQ-20 ที่ทดสอบแล้วไม่รองรับ)" },
            ].map((o) => (
              <label key={o.v} className="hstack" style={{
                gap: 8, alignItems: "flex-start", cursor: "pointer", padding: "8px 10px",
                border: "1px solid " + (settings.engine === o.v ? "var(--brand)" : "var(--border)"),
                background: settings.engine === o.v ? "var(--brand-50, rgba(37,99,235,.06))" : "transparent",
                borderRadius: 6,
              }}>
                <input type="radio" name="dm-engine" checked={settings.engine === o.v}
                  style={{ marginTop: 2 }} onChange={() => update({ engine: o.v })} />
                <span>
                  <div style={{ fontSize: 13, fontWeight: settings.engine === o.v ? 600 : 400 }}>{o.t}</div>
                  <div className="dim" style={{ fontSize: 11.5, marginTop: 2 }}>{o.d}</div>
                </span>
              </label>
            ))}
          </div>
          {settings.engine === "escp" && escpUnsupported && (
            <div style={{
              marginTop: 8, padding: "8px 10px", borderRadius: 6, fontSize: 12,
              background: "var(--warn-50, #FEF3C7)", color: "var(--warn, #92400E)",
            }}>
              <Icon name="info" size={11} /> แอปเวอร์ชันนี้ยังพิมพ์ ESC/P ไม่ได้ — กรุณาอัปเดตแอปก่อน
              มิฉะนั้นกดพิมพ์แล้วจะขึ้นข้อความเตือนแทนที่จะพิมพ์ออกมา
            </div>
          )}
        </div>

        <div className="divider"></div>

        {settings.engine === "escp" ? (
        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 4 }}>ตั้งค่า ESC/P</div>
          <div className="dim" style={{ fontSize: 12, marginBottom: 8 }}>
            ตัวอักษรกว้างเท่ากันหมด ยิ่งถี่ยิ่งใส่ได้เยอะแต่ตัวเล็กลง — ที่ {settings.escpCpi} ตัว/นิ้ว
            ได้ประมาณ <b>{Math.min((window.ESCP_CPI && window.ESCP_CPI[settings.escpCpi]
              ? window.ESCP_CPI[settings.escpCpi].max : 80), Math.floor(9 * settings.escpCpi))} ตัว/บรรทัด</b> บนกระดาษ 9 นิ้ว
          </div>
          <div className="hstack" style={{ gap: 12 }}>
            <div className="field" style={{ flex: 1.4 }}>
              <label>ความถี่ตัวอักษร</label>
              <select className="select" value={String(settings.escpCpi)}
                onChange={(e) => update({ escpCpi: Number(e.target.value) })}>
                {(window.ESCP_CPI_LIST || []).map((c) => <option key={c.v} value={String(c.v)}>{c.label}</option>)}
              </select>
            </div>
            <div className="field" style={{ flex: 1 }}>
              <label>คุณภาพ</label>
              <select className="select" value={settings.escpQuality}
                onChange={(e) => update({ escpQuality: e.target.value })}>
                <option value="lq">LQ — คมที่สุด (ช้ากว่า)</option>
                <option value="draft">Draft — เร็วที่สุด</option>
              </select>
            </div>
          </div>
          <div className="hstack" style={{ gap: 12, marginTop: 10 }}>
            <div className="field" style={{ flex: 2 }}>
              <label>ตารางอักขระไทย</label>
              <select className="select" value={settings.escpTable}
                onChange={(e) => update({ escpTable: e.target.value })}>
                {(window.ESCP_TABLES || []).map((t) => <option key={t.v} value={t.v}>{t.label}</option>)}
              </select>
            </div>
            <div className="field" style={{ flex: 1 }}>
              <label>ท้ายบรรทัด</label>
              <select className="select" value={settings.escpLineEnd}
                onChange={(e) => update({ escpLineEnd: e.target.value })}>
                <option value="crlf">CR + LF (ปกติ)</option>
                <option value="lf">LF อย่างเดียว</option>
              </select>
            </div>
            <div className="field" style={{ flex: 0.7 }}>
              <label>เยื้องซ้าย</label>
              <input className="input num" type="text" inputMode="numeric" placeholder="0"
                value={rawIndent}
                onChange={(e) => {
                  const v = e.target.value.replace(/[^0-9]/g, "");
                  setRawIndent(v);
                  update({ escpIndent: Math.max(0, Math.min(20, Number(v) || 0)) });
                }} />
            </div>
          </div>
          <div className="field" style={{ marginTop: 10 }}>
            <label>สระ/วรรณยุกต์กินที่กี่ช่อง</label>
            <select className="select" value={settings.escpMarks}
              onChange={(e) => update({ escpMarks: e.target.value })}>
              {(window.ESCP_MARK_MODES || []).map((m) => <option key={m.v} value={m.v}>{m.label}</option>)}
            </select>
            <div className="dim" style={{ fontSize: 11.5, marginTop: 4 }}>
              ช่องนี้แค่บอกระบบว่าเครื่อง<b>นับ</b>สระเป็นช่องหรือไม่ เพื่อให้คอลัมน์ในตารางตรง
              <br /><b>&ldquo;ให้ระบบพิมพ์ทับเอง&rdquo;</b> ใช้ได้เฉพาะเครื่องที่วาดสระไว้ครึ่งบน-ครึ่งล่างของช่อง
              ถ้าเครื่องวาดสระเป็นตัวเต็มช่องเหมือนพยัญชนะ พิมพ์ทับแล้วจะกลายเป็นก้อนดำอ่านไม่ออก
              <br />เครื่องที่วาดสระเป็นตัวเต็มช่อง ต้องใช้ปุ่ม <b>หาโหมดไทย</b> ด้านล่างแทน
            </div>
          </div>

          <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
            <div className="dim" style={{ fontSize: 11.5, marginBottom: 6 }}>
              <b>ภาษาไทยออกมาเป็นตัวประหลาด หรือคอลัมน์เหลื่อม?</b> ทั้งสองอย่างขึ้นกับรุ่นเครื่อง —
              กดปุ่มนี้จะพิมพ์ประโยคไทยซ้ำ 5 บรรทัดด้วยตารางอักขระคนละค่า (บรรทัดไหนอ่านออกก็เลือกค่านั้น)
              แล้วต่อด้วยชุดทดสอบแนวคอลัมน์ A ถึง F ที่ทุกบรรทัดตั้งใจให้กว้างเท่ากัน —
              ถ้าขีด <b>|</b> ของบรรทัด B C D E เลื่อนออกไปทางขวา แปลว่าเครื่องไม่พิมพ์สระทับตัว —
              ให้ตั้งช่องข้างบนเป็น <b>&ldquo;กินที่ทุกตัว&rdquo;</b> เพื่อให้คอลัมน์ตรงไว้ก่อน
              แล้วใช้ปุ่ม <b>หาโหมดไทย</b> เพื่อหาโหมดที่เครื่องประกอบสระให้เอง
            </div>
            <div className="hstack" style={{ gap: 6, flexWrap: "wrap" }}>
              {DM_PAPER_LIST.map((p) => (
                <button key={p.key} className="btn sm" disabled={!!testing}
                  onClick={() => doProbe(p.key)}>
                  {testing === "probe:" + p.key
                    ? <><span className="spinner" /> กำลังส่ง...</>
                    : <><Icon name="search" size={12} /> หาตารางไทย · {p.short}</>}
                </button>
              ))}
            </div>

            <div className="dim" style={{ fontSize: 11.5, margin: "12px 0 6px" }}>
              <b>อยากให้เครื่องจัดสระเองแทนที่จะให้ระบบพิมพ์ทับ?</b> เครื่องไทยมักมีรหัสตารางไทยหลายแบบ
              ต่างกันตรงวิธีวางสระ/วรรณยุกต์ — ปุ่มนี้จะสั่งเครื่องไล่เปลี่ยนรหัสทีละค่าแล้วพิมพ์ให้ดู
              ถ้าเจอบรรทัดที่สระทับตัวถูกต้อง ให้กรอกรหัสนั้นในช่องข้างล่าง
            </div>
            <div className="hstack" style={{ gap: 6, flexWrap: "wrap", marginBottom: 8 }}>
              {DM_PAPER_LIST.map((p) => (
                <button key={p.key} className="btn sm" disabled={!!testing}
                  onClick={() => doThaiProbe(p.key)}>
                  {testing === "thai:" + p.key
                    ? <><span className="spinner" /> กำลังส่ง...</>
                    : <><Icon name="search" size={12} /> หาโหมดไทย · {p.short}</>}
                </button>
              ))}
            </div>
            <div className="field">
              <label>รหัสตารางไทยของเครื่อง (ว่างไว้ถ้าไม่รู้)</label>
              <input className="input num" type="text" inputMode="numeric" placeholder="ปล่อยว่าง"
                style={{ maxWidth: 160 }}
                value={rawThai}
                onChange={(e) => {
                  const v = e.target.value.replace(/[^0-9]/g, "").slice(0, 3);
                  setRawThai(v);
                  update({ escpThaiCode: v === "" ? "" : String(Math.min(255, Number(v))) });
                }} />
            </div>
          </div>
          <div className="dim" style={{ fontSize: 11.5, marginTop: 10 }}>
            มีบรรทัดว่างแทรกทุกบรรทัด = ปิด Auto Line Feed ที่เครื่อง หรือเปลี่ยนท้ายบรรทัดเป็น LF อย่างเดียว
            <br />ข้อความเริ่มไม่ตรงขอบ = ขยับกระดาษที่ตัวเครื่อง หรือเพิ่มค่าเยื้องซ้าย
          </div>
        </div>
        ) : (
        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 4 }}>ความคมชัดของตัวอักษร</div>
          <div className="dim" style={{ fontSize: 12, marginBottom: 8 }}>
            หัวเข็มพิมพ์ด้วยจุด สระบน-ล่างและวรรณยุกต์ของตัวเล็กๆ จะเละก่อนเพื่อน —
            ถ้าอ่านไม่ชัด ให้<b>ขยายขนาด</b>ก่อน แล้วค่อยลองเปิด<b>เข้มพิเศษ</b>
          </div>
          <div className="hstack" style={{ gap: 6, flexWrap: "wrap", marginBottom: 10 }}>
            {DM_PRESETS.map((p) => {
              const on = settings.fontKey === p.v.fontKey
                && Number(settings.fontScale) === p.v.fontScale
                && !!settings.boldAll === p.v.boldAll;
              return (
                <button key={p.key} className={"btn sm" + (on ? " primary" : "")} title={p.hint}
                  onClick={() => update(p.v)}>
                  {on ? <Icon name="check" size={12} /> : null} {p.name}
                </button>
              );
            })}
          </div>
          <div className="dim" style={{ fontSize: 11.5, marginBottom: 10 }}>
            {(DM_PRESETS.find((p) =>
              settings.fontKey === p.v.fontKey
              && Number(settings.fontScale) === p.v.fontScale
              && !!settings.boldAll === p.v.boldAll) || {}).hint
              || "ตั้งค่าเอง — ปรับทีละช่องด้านล่าง"}
          </div>

          <div className="hstack" style={{ gap: 12, alignItems: "flex-end" }}>
            <div className="field" style={{ flex: 1 }}>
              <label>ขนาดตัวอักษร</label>
              <select className="select" value={String(settings.fontScale)}
                onChange={(e) => update({ fontScale: Number(e.target.value) })}>
                {DM_SCALES.map((x) => <option key={x.v} value={String(x.v)}>{x.label}</option>)}
              </select>
            </div>
            <div className="field" style={{ flex: 1.6 }}>
              <label>แบบอักษร</label>
              <select className="select" value={settings.fontKey}
                onChange={(e) => update({ fontKey: e.target.value })}>
                {Object.values(DM_FONTS).map((f) => <option key={f.key} value={f.key}>{f.label}</option>)}
              </select>
            </div>
          </div>
          <label className="hstack" style={{ gap: 8, marginTop: 10, fontSize: 13, cursor: "pointer", alignItems: "flex-start" }}>
            <input type="checkbox" checked={!!settings.boldAll} style={{ marginTop: 2 }}
              onChange={(e) => update({ boldAll: e.target.checked })} />
            <span>
              เข้มพิเศษ (เดินเส้นหนาขึ้นทั้งฉบับ)
              <div className="dim" style={{ fontSize: 11.5, marginTop: 2 }}>
                หัวเข็มจะตีซ้ำให้เข้มขึ้น พิมพ์ช้าลงและเปลืองผ้าหมึกกว่าเดิม —
                ถ้าตัวอักษรกลายเป็นก้อนทึบจนอ่านไม่ออก ให้ปิดแล้วขยายขนาดแทน
              </div>
            </span>
          </label>
          <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border)" }}>
            <div className="dim" style={{ fontSize: 11.5, marginBottom: 6 }}>
              ตัวใหญ่ขึ้นแล้วใส่งวดได้น้อยลง — <b>ใบสรุปรายหลัง</b> ใส่ได้กี่งวดต่อแผ่น (วัดจากของจริง)
            </div>
            <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11.5 }}>
              <thead>
                <tr style={{ color: "var(--text-2)" }}>
                  <th style={{ textAlign: "left", fontWeight: 500, padding: "2px 0" }}>กระดาษ</th>
                  {DM_SCALES.map((x) => (
                    <th key={x.v} style={{ textAlign: "right", fontWeight: 500, padding: "2px 0", width: 46 }}>
                      {Math.round(x.v * 100)}%
                    </th>
                  ))}
                </tr>
              </thead>
              <tbody>
                {DM_PAPER_LIST.map((p) => {
                  const cap = DM_CAPACITY[p.key] || [];
                  const used = Object.values(settings.papers).includes(p.key);
                  return (
                    <tr key={p.key} style={{ borderTop: "1px solid var(--border)", opacity: used ? 1 : 0.5 }}>
                      <td style={{ padding: "3px 0", fontWeight: used ? 600 : 400 }}>{p.short}</td>
                      {cap.map((n, i) => (
                        <td key={i} className="num" style={{ textAlign: "right", padding: "3px 0" }}>{n}</td>
                      ))}
                    </tr>
                  );
                })}
              </tbody>
            </table>
            <div className="dim" style={{ fontSize: 11.5, marginTop: 8 }}>
              เกินจากนี้จะไหลลงแผ่นถัดไป (กระดาษต่อเนื่องจะเสียไปทั้งแผ่น) —
              ถ้าอยากได้ตัวใหญ่แต่ยังใส่ได้เยอะ ให้เปลี่ยนใบสรุปรายหลังไปใช้ <b>A5 แนวตั้ง</b> หรือ <b>9 × 11 นิ้ว</b>
              <br />กด <b>พิมพ์หน้าทดสอบ</b> ด้านล่างเพื่อดูตัวอย่างทุกขนาดที่รายงานจริงใช้ แล้วเทียบว่าบรรทัดไหนเริ่มอ่านไม่ออก
            </div>
          </div>
        </div>
        )}

        <div className="divider"></div>

        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 4 }}>ปรับขอบกระดาษละเอียด</div>
          <div className="dim" style={{ fontSize: 12, marginBottom: 8 }}>
            ใส่ค่าบวกเพื่อขยับเนื้อหา<b>เข้าด้านใน</b> · ค่าลบเพื่อขยับ<b>ออกด้านนอก</b> —
            ใช้เมื่อข้อความล้ำรอยปรุหรือชิดขอบเกินไป
          </div>
          <div className="hstack" style={{ gap: 12 }}>
            <div className="field" style={{ flex: 1 }}>
              <label>ซ้าย-ขวา (มม.)</label>
              <input className="input num" type="text" inputMode="numeric" placeholder="0"
                value={rawX}
                onChange={(e) => setOffset("x", e.target.value, setRawX)} />
            </div>
            <div className="field" style={{ flex: 1 }}>
              <label>บน-ล่าง (มม.)</label>
              <input className="input num" type="text" inputMode="numeric" placeholder="0"
                value={rawY}
                onChange={(e) => setOffset("y", e.target.value, setRawY)} />
            </div>
            <div style={{ flex: 1.4, fontSize: 12, color: "var(--text-2)", paddingTop: 18 }}>
              {(() => {
                const p = DM_PAPERS[settings.papers.house] || DM_PAPERS["9x5.5"];
                const pad = dmPads(p, { x: settings.offsetXmm, y: settings.offsetYmm });
                return `ขอบจริงที่ใช้: ซ้าย-ขวา ${(pad.padH * MM_PER_IN).toFixed(1)} มม. · บน-ล่าง ${(pad.padV * MM_PER_IN).toFixed(1)} มม.`;
              })()}
            </div>
          </div>
        </div>

        <div className="divider"></div>

        <div>
          <div style={{ fontWeight: 500, fontSize: 13, marginBottom: 4 }}>ทดสอบการพิมพ์</div>
          <div className="dim" style={{ fontSize: 12, marginBottom: 8 }}>
            พิมพ์หน้าทดสอบเพื่อตรวจว่ากระดาษตรงตำแหน่งและภาษาไทยไม่เพี้ยน ก่อนพิมพ์เอกสารจริง
          </div>
          <div className="hstack" style={{ gap: 6, flexWrap: "wrap" }}>
            {DM_PAPER_LIST.map((p) => (
              <button key={p.key} className="btn sm" disabled={!!testing}
                onClick={() => doTest(p.key)}>
                {testing === p.key
                  ? <><span className="spinner" /> กำลังส่ง...</>
                  : <><Icon name="print" size={12} /> ทดสอบ {p.w}×{p.h}"</>}
              </button>
            ))}
          </div>
        </div>

        <div className="dim" style={{ fontSize: 11.5, padding: "8px 10px", background: "var(--surface-2)", borderRadius: 6 }}>
          <Icon name="info" size={11} /> รายงาน Dot Matrix เป็นแบบขาว-ดำล้วน ไม่มีพื้นหลัง เพื่อให้พิมพ์เร็วและประหยัดผ้าหมึก
          — ต่างจากรายงาน PDF ปกติที่ออกแบบสำหรับเครื่องพิมพ์เลเซอร์/อิงค์เจ็ท
        </div>
      </div>
  );
}

// การ์ดในหน้า "ตั้งค่า"
function DotMatrixSettingsCard() {
  // อยู่ในแอปแต่เป็นเวอร์ชันเก่า → บอกให้อัปเดตแทนที่จะหายไปเฉยๆ
  if (dmNeedsAppUpdate()) {
    return (
      <div className="card" style={{ marginBottom: 14 }}>
        <div className="card-head"><h3>เครื่องพิมพ์ Dot Matrix</h3></div>
        <div className="card-body">
          <div className="dim" style={{ fontSize: 13 }}>
            ระบบพิมพ์ Dot Matrix ต้องใช้แอปเวอร์ชัน 1.2.0 ขึ้นไป —
            ไปที่เมนู <b>ไฟล์ → ตรวจสอบการอัปเดตแอป</b> เพื่ออัปเดตก่อน
          </div>
        </div>
      </div>
    );
  }
  if (!dmAvailable()) return null;
  return (
    <div className="card" style={{ marginBottom: 14 }}>
      <div className="card-head"><h3>เครื่องพิมพ์ Dot Matrix</h3></div>
      <div className="card-body"><DotMatrixSettingsBody /></div>
    </div>
  );
}

// Modal เดียวกัน เปิดจากเมนู ไฟล์ → ตั้งค่าเครื่องพิมพ์ Dot Matrix
function DotMatrixSettingsModal({ onClose }) {
  return (
    <Modal title="ตั้งค่าเครื่องพิมพ์ Dot Matrix" icon="print" size="lg" onClose={onClose} foot={
      <button className="btn primary" onClick={onClose}><Icon name="check" size={13} /> เสร็จสิ้น</button>
    }>
      <DotMatrixSettingsBody />
    </Modal>
  );
}

// รับคำสั่งจากเมนูของแอป
function DotMatrixHost() {
  const [show, setShow] = useStateDM(false);
  useEffectDM(() => {
    if (!window.Desktop || !window.Desktop.onOpenPrinterSettings) return;
    // แอปรุ่นเก่ายังไม่คืนฟังก์ชันถอด listener — ต้องเช็กก่อนเรียก
    const off = window.Desktop.onOpenPrinterSettings(() => setShow(true));
    return () => { if (typeof off === "function") off(); };
  }, []);
  if (!show) return null;
  return <DotMatrixSettingsModal onClose={() => setShow(false)} />;
}

Object.assign(window, {
  DM_PAPERS, DM_REPORTS, dmAvailable, dmNeedsAppUpdate,
  getDmSettings, saveDmSettings, dmPaperFor, dmStatus, dmFmt, dmDate,
  dmPrint, dmPrintHouse, dmPrintContract, dmPrintLedger, dmPrintTest,
  dmPrintTableProbe, dmPrintThaiModeProbe,
  dmUseEscp, dmEscpBlocked,
  DmPrintButton, DotMatrixSettingsCard, DotMatrixSettingsModal, DotMatrixHost,
});
