403Webshell
Server IP : 172.67.189.169  /  Your IP : 216.73.216.59
Web Server : Apache/2
System : Linux cat167-197.static.lnwhostname.com 5.10.0-20-amd64 #1 SMP Debian 5.10.158-2 (2022-12-13) x86_64
User : paphayomho ( 1042)
PHP Version : 5.6.40
Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname
MySQL : ON  |  cURL : ON  |  WGET : OFF  |  Perl : OFF  |  Python : OFF  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/paphayomho/domains/paphayomhospital.go.th/private_html/FDH2569/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/paphayomho/domains/paphayomhospital.go.th/private_html/FDH2569/PP.html
<!doctype html>
<html lang="th">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>คัดกรองฟลูออไรด์กลุ่มเสี่ยง อายุ 25-59 ปี</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Thai:wght@400;500;600;700&display=swap" rel="stylesheet">
  <style>
    :root {
      color-scheme: light;
      --bg: #edf7f6;
      --surface: #fbfefd;
      --panel: #ffffff;
      --ink: #173037;
      --muted: #5a7074;
      --line: #cfe1df;
      --line-strong: #a9c6c2;
      --brand: #007c73;
      --brand-dark: #07524e;
      --accent: #c75f2a;
      --iron: #8b6f2e;
      --plasma: #f2cf7a;
      --soft: #e4f4f1;
      --cool: #eaf3fb;
      --warn: #fff7df;
      --warn-line: #d7ad45;
      --success: #0f6f4f;
      --danger: #b42318;
      --radius: 8px;
      --shadow: 0 14px 34px rgba(42, 53, 62, 0.09);
    }

    * { box-sizing: border-box; }

    body {
      margin: 0;
      min-height: 100vh;
      background:
        linear-gradient(180deg, rgba(0, 124, 115, 0.09), rgba(237, 247, 246, 0) 260px),
        repeating-linear-gradient(90deg, rgba(23, 48, 55, 0.024) 0 1px, transparent 1px 84px),
        var(--bg);
      color: var(--ink);
      font-family: "IBM Plex Sans Thai", Tahoma, "Noto Sans Thai", Arial, sans-serif;
      line-height: 1.7;
      overflow-wrap: break-word;
    }

    button, input, select {
      font: inherit;
    }

    .shell {
      width: min(1240px, calc(100vw - 32px));
      margin: 0 auto;
      padding: 30px 0 44px;
    }

    .topbar {
      position: relative;
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 24px;
      margin-bottom: 20px;
      padding: 22px 0 18px 26px;
      border-bottom: 1px solid var(--line);
    }

    .topbar::before {
      content: "";
      position: absolute;
      left: 0;
      top: 22px;
      bottom: 20px;
      width: 8px;
      border-radius: 999px;
      background:
        linear-gradient(
          180deg,
          var(--brand) 0 34%,
          var(--plasma) 34% 48%,
          var(--iron) 48% 70%,
          #294c5a 70% 100%
        );
    }

    h1 {
      margin: 0 0 8px;
      max-width: 960px;
      font-size: clamp(25px, 3vw, 38px);
      line-height: 1.35;
      font-weight: 700;
      letter-spacing: 0;
    }

    .lead {
      margin: 0;
      max-width: 900px;
      color: var(--muted);
      font-size: 15.5px;
      line-height: 1.75;
    }

    .pill {
      flex: 0 0 auto;
      min-width: 170px;
      border: 1px solid var(--line);
      background: rgba(255, 255, 255, 0.82);
      border-radius: 999px;
      padding: 8px 14px;
      color: var(--brand-dark);
      font-weight: 600;
      text-align: center;
      box-shadow: 0 8px 22px rgba(42, 53, 62, 0.07);
      font-variant-numeric: tabular-nums;
    }

    .band {
      background: rgba(255, 255, 255, 0.9);
      border: 1px solid var(--line);
      border-radius: var(--radius);
      padding: 20px;
      box-shadow: var(--shadow);
      margin-bottom: 16px;
    }

    .upload-row {
      display: grid;
      grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
      gap: 16px;
      align-items: stretch;
    }

    .dropzone {
      border: 1.5px dashed var(--line-strong);
      border-radius: var(--radius);
      padding: 24px;
      background:
        linear-gradient(135deg, rgba(138, 21, 56, 0.045), transparent 42%),
        var(--surface);
      display: grid;
      gap: 12px;
      min-height: 170px;
      align-content: center;
    }

    .dropzone.drag {
      border-color: var(--brand);
      background: var(--soft);
    }

    .dropzone strong {
      font-size: 18px;
      font-weight: 700;
    }

    .file-actions {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }

    .file-input {
      position: absolute;
      width: 1px;
      height: 1px;
      opacity: 0;
      overflow: hidden;
    }

    .btn, .file-label {
      border: 0;
      border-radius: var(--radius);
      padding: 10px 15px;
      background: var(--brand);
      color: white;
      font-weight: 600;
      cursor: pointer;
      min-height: 42px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      text-decoration: none;
      transition: background-color 160ms ease, transform 160ms ease, border-color 160ms ease;
    }

    .btn:hover, .file-label:hover {
      background: var(--brand-dark);
      transform: translateY(-1px);
    }

    .btn.secondary {
      background: #f4f7f8;
      color: var(--ink);
      border: 1px solid var(--line);
    }

    .btn.secondary:hover {
      background: #e9eff2;
    }

    .btn.danger {
      background: var(--danger);
    }

    .btn:disabled {
      cursor: not-allowed;
      opacity: 0.48;
      transform: none;
    }

    .btn:focus-visible, .file-label:focus-visible {
      outline: 3px solid rgba(138, 21, 56, 0.22);
      outline-offset: 2px;
    }

    .rule-box {
      border: 1px solid var(--line);
      border-radius: var(--radius);
      padding: 16px;
      background: linear-gradient(180deg, #fbfcfd, #f7fafb);
    }

    .rule-box h2, .section-title {
      margin: 0 0 12px;
      font-size: 16px;
      line-height: 1.55;
      font-weight: 700;
    }

    .rule-grid {
      display: grid;
      gap: 10px;
      color: var(--muted);
      font-size: 14.5px;
    }

    code {
      background: #eef4f5;
      border: 1px solid #d8e2e5;
      border-radius: 5px;
      padding: 1px 5px;
      color: #253845;
      font-family: Consolas, "Courier New", monospace;
      font-size: 0.95em;
    }

    .metrics {
      display: grid;
      grid-template-columns: repeat(7, minmax(148px, 1fr));
      gap: 12px;
    }

    .metric {
      border: 1px solid var(--line);
      border-radius: var(--radius);
      padding: 14px;
      background: var(--surface);
      min-height: 94px;
      box-shadow: inset 0 3px 0 rgba(138, 21, 56, 0.08);
    }

    .metric span {
      display: block;
      color: var(--muted);
      font-size: 13px;
      line-height: 1.55;
      margin-bottom: 8px;
    }

    .metric strong {
      display: block;
      font-size: clamp(22px, 2vw, 30px);
      line-height: 1.1;
      font-weight: 700;
      color: var(--ink);
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }

    .metric small {
      color: var(--muted);
      display: block;
      margin-top: 7px;
      line-height: 1.5;
    }

    .toolbar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 12px;
      flex-wrap: wrap;
      margin-bottom: 12px;
    }

    .status {
      color: var(--muted);
      font-size: 14px;
      line-height: 1.65;
    }

    .alert {
      border: 1px solid var(--warn-line);
      background: var(--warn);
      color: #754400;
      border-radius: var(--radius);
      padding: 12px 14px;
      margin-top: 12px;
      display: none;
    }

    .alert.show {
      display: block;
    }

    .table-wrap {
      overflow: auto;
      border: 1px solid var(--line);
      border-radius: var(--radius);
      background: white;
      max-height: 390px;
    }

    table {
      width: 100%;
      border-collapse: collapse;
      min-width: 720px;
      font-size: 13.5px;
      font-variant-numeric: tabular-nums;
    }

    th, td {
      border-bottom: 1px solid #edf2f4;
      padding: 10px 11px;
      text-align: left;
      white-space: nowrap;
      line-height: 1.65;
    }

    th {
      position: sticky;
      top: 0;
      background: #f2f6f7;
      z-index: 1;
      color: #2c3e48;
      font-weight: 700;
    }

    tbody tr:nth-child(even) td {
      background: #fbfcfd;
    }

    tbody tr:hover td {
      background: #fff7f8;
    }

    tr:last-child td {
      border-bottom: 0;
    }

    .ok { color: var(--success); font-weight: 700; }
    .bad { color: var(--danger); font-weight: 700; }
    .muted { color: var(--muted); }

    .footer-note {
      margin: 14px 0 0;
      color: var(--muted);
      font-size: 13px;
      line-height: 1.7;
    }

    @media (max-width: 1180px) {
      .metrics {
        grid-template-columns: repeat(3, minmax(0, 1fr));
      }
    }

    @media (max-width: 860px) {
      .topbar, .upload-row {
        grid-template-columns: 1fr;
        display: grid;
      }

      .pill {
        width: 100%;
      }

      .metrics {
        grid-template-columns: repeat(2, minmax(0, 1fr));
      }
    }

    @media (max-width: 560px) {
      .shell {
        width: min(100vw - 20px, 1180px);
        padding-top: 18px;
      }

      .metrics {
        grid-template-columns: 1fr;
      }

      .dropzone, .band {
        padding: 14px;
      }

      h1 {
        font-size: 23px;
      }
    }

    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        scroll-behavior: auto !important;
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <main class="shell">
    <header class="topbar">
      <div>
        <h1>คัดกรองฟลูออไรด์กลุ่มเสี่ยง อายุ 25-59 ปี</h1>
        <p class="lead">อัปโหลดชุด 16 แฟ้ม แล้วคัดกรองเคสสิทธิ UCS อายุ 25-59 ปีที่พบ ADP 4|15001, ODX.DIAG = K020 หรือ ICD9 2387020/2387021 พร้อมรวมจำนวนเคส ยอด 100 บาทต่อเคส เติม ODX K020, ADP 4|15001 และ ICD9 ที่ขาดเมื่อยังไม่มี แล้วส่งออกกลับเป็น 16 แฟ้มโครงสร้างเดิมสำหรับ FDH</p>
      </div>
      <div class="pill" id="statusPill">พร้อมใช้งาน</div>
    </header>

    <section class="band">
      <div class="upload-row">
        <div class="dropzone" id="dropzone">
          <strong>อัปโหลด 16 แฟ้ม `.txt`</strong>
          <span class="muted">เลือกหลายไฟล์พร้อมกัน เลือกทั้งโฟลเดอร์ชุด FDH หรืออัปโหลดไฟล์ .zip ได้</span>
          <div class="file-actions">
            <label class="file-label" for="fileInput">เลือกไฟล์</label>
            <input class="file-input" id="fileInput" type="file" multiple accept=".txt,.zip,text/plain,application/zip">
            <label class="file-label" for="folderInput">เลือกโฟลเดอร์</label>
            <input class="file-input" id="folderInput" type="file" multiple webkitdirectory>
            <button class="btn secondary" id="clearButton" type="button">ล้างข้อมูล</button>
          </div>
          <span class="status" id="uploadStatus">ยังไม่ได้อัปโหลดไฟล์</span>
        </div>

        <div class="rule-box">
          <h2>เงื่อนไขคัดกรอง</h2>
          <div class="rule-grid">
            <div>จาก <code>ADP.txt</code>: <code>TYPE = 4</code> และ <code>CODE = 15001</code></div>
            <div>หรือจาก <code>ODX.txt</code>: <code>DIAG = K020</code></div>
            <div>หรือจาก <code>OOP.txt/IOP.txt</code>: ICD9 <code>2387020</code> หรือ <code>2387021</code></div>
            <div>จาก <code>INS.txt</code>: <code>INSCL = UCS</code> และจาก <code>PAT.txt</code>: อายุ <code>25-59 ปี</code> ณ วันบริการ รวมมูลค่า <code>100 บาท/เคส</code></div>
          </div>
        </div>
      </div>
      <div class="alert" id="alertBox"></div>
    </section>

    <section class="band">
      <div class="toolbar">
        <h2 class="section-title">ผลรวม</h2>
        <button class="btn" id="exportButton" type="button" disabled>Export ZIP 16 แฟ้ม</button>
      </div>
      <div class="metrics">
        <div class="metric">
          <span>เคสเข้าเกณฑ์</span>
          <strong id="caseCount">0</strong>
          <small>นับ visit ไม่ซ้ำจาก AN/SEQ</small>
        </div>
        <div class="metric">
          <span>มูลค่ารวม 100 บาท/เคส</span>
          <strong id="chtTotal">0.00</strong>
          <small>บาท</small>
        </div>
        <div class="metric">
          <span>ยอด ADP 4|15001 เดิม</span>
          <strong id="adpTotal">0.00</strong>
          <small>บาท</small>
        </div>
        <div class="metric">
          <span>แถวที่จะ export</span>
          <strong id="rowTotal">0</strong>
          <small>รวมทุกแฟ้ม</small>
        </div>
        <div class="metric">
          <span>ต้องเติม ODX K020</span>
          <strong id="autofillCount">0</strong>
          <small>เคสที่ยังไม่มี K020</small>
        </div>
        <div class="metric">
          <span>ต้องเติม ADP 4|15001</span>
          <strong id="autofillAdpCount">0</strong>
          <small>เคสที่ยังไม่มี ADP 15001</small>
        </div>
        <div class="metric">
          <span>ต้องเติม ICD9</span>
          <strong id="autofillIcd9Count">0</strong>
          <small>รหัส 2387020/2387021 ที่ยังไม่มี</small>
        </div>
        <div class="metric">
          <span>ลบ refer</span>
          <strong id="referCleanupCount">0</strong>
          <small>แถว</small>
        </div>
      </div>
      <div class="file-actions" style="margin-top: 12px;">
        <button class="btn secondary" id="autofillButton" type="button" disabled>Autofill ODX K020 + ADP 4|15001 + ICD9</button>
        <span class="status" id="autofillStatus">ยังไม่มีรายการที่ต้องเติม</span>
      </div>
      <div class="file-actions" style="margin-top: 12px;">
        <button class="btn secondary" id="referCleanupButton" type="button" disabled>ลบ refer</button>
        <span class="status" id="referCleanupStatus">ยังไม่ได้ลบ refer</span>
      </div>
    </section>

    <section class="band">
      <h2 class="section-title">สถานะแฟ้ม</h2>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>แฟ้ม</th>
              <th>สถานะ</th>
              <th>แถวต้นฉบับ</th>
              <th>แถวหลังกรอง</th>
              <th>หมายเหตุ</th>
            </tr>
          </thead>
          <tbody id="fileTable"></tbody>
        </table>
      </div>
    </section>

    <section class="band">
      <h2 class="section-title">ผล Autofill ODX, ADP และ ICD9</h2>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>#</th>
              <th>สถานะ</th>
              <th>HN</th>
              <th>แฟ้ม</th>
              <th>SEQ</th>
              <th>วันที่</th>
              <th>รหัส</th>
              <th>รายละเอียด</th>
              <th>ยอด</th>
              <th>Field ที่เติม</th>
            </tr>
          </thead>
          <tbody id="autofillTable"></tbody>
        </table>
      </div>
    </section>

    <section class="band">
      <h2 class="section-title">ผลลบ refer</h2>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>#</th>
              <th>สถานะ</th>
              <th>HN</th>
              <th>แฟ้ม</th>
              <th>วันที่</th>
              <th>รหัส</th>
              <th>รายละเอียด</th>
              <th>SEQ</th>
              <th>ยอด</th>
            </tr>
          </thead>
          <tbody id="referCleanupTable"></tbody>
        </table>
      </div>
    </section>

    <section class="band">
      <h2 class="section-title">Preview เคสเข้าเกณฑ์</h2>
      <div class="table-wrap">
        <table>
          <thead>
            <tr>
              <th>#</th>
              <th>ชนิด</th>
              <th>HN</th>
              <th>AN</th>
              <th>SEQ</th>
              <th>อายุ</th>
              <th>สาเหตุ</th>
              <th>มูลค่า</th>
            </tr>
          </thead>
          <tbody id="caseTable"></tbody>
        </table>
      </div>
      <p class="footer-note">หมายเหตุ: เครื่องมือนี้ทำงานใน browser ฝั่งเครื่องผู้ใช้ ไฟล์ไม่ถูกส่งออกไปยัง server</p>
    </section>
  </main>

  <script>
    const EXPECTED_FILES = [
      "ADP", "AER", "CHA", "CHT", "DRU", "IDX", "INS", "IOP",
      "IPD", "IRF", "LVD", "ODX", "OOP", "OPD", "ORF", "PAT"
    ];
    const TARGET_ADP_TYPE = "4";
    const TARGET_ADP_CODE = "15001";
    const TARGET_ODX_DIAG = "K020";
    const TARGET_ICD9_CODES = new Set(["2387020", "2387021"]);
    const AUTOFILL_ICD9_CODES = ["2387020", "2387021"];
    const AUTOFILL_ODX_DIAG = "K020";
    const AUTOFILL_ADP_TYPE = "4";
    const AUTOFILL_ADP_CODE = "15001";
    const AUTOFILL_ADP_RATE = "100.00";
    const MIN_AGE = 25;
    const MAX_AGE = 59;
    const CASE_RATE_CENTS = 10000;
    const REFER_ADP_REMOVE = new Set(["3|S1801", "3|S1802"]);
    const REFER_CHA_CHRGITEM = "J1";
    const REFER_CHA_AMOUNT_CENTS = 43500;
    const FIELD_NAMES = [
      "HN", "AN", "SEQ", "PERSON_ID", "TYPE", "CODE", "DIAG", "TOTAL", "AMOUNT",
      "SERVPRICE", "DATE", "DATEDX", "DATEOPD", "DATEADM", "DATEIN", "DATEOUT",
      "DOB", "SEX", "NATION", "CLINIC", "DXTYPE", "DRDX", "CHRGITEM", "OPER",
      "OPTYPE", "ICD9", "PROCED", "TMLTCODE", "QTY", "RATE", "TOTCOPAY", "BI",
      "ITEMSRC", "PROVIDER", "INSCL", "SUBTYPE", "TIMEIN", "TIMEOUT"
    ];
    const asciiDecoder = new TextDecoder("iso-8859-1");

    const state = {
      tables: new Map(),
      result: null,
      autofillEnabled: false,
      referCleanupEnabled: false
    };

    const el = {
      dropzone: document.querySelector("#dropzone"),
      fileInput: document.querySelector("#fileInput"),
      folderInput: document.querySelector("#folderInput"),
      clearButton: document.querySelector("#clearButton"),
      autofillButton: document.querySelector("#autofillButton"),
      referCleanupButton: document.querySelector("#referCleanupButton"),
      exportButton: document.querySelector("#exportButton"),
      uploadStatus: document.querySelector("#uploadStatus"),
      statusPill: document.querySelector("#statusPill"),
      alertBox: document.querySelector("#alertBox"),
      caseCount: document.querySelector("#caseCount"),
      chtTotal: document.querySelector("#chtTotal"),
      adpTotal: document.querySelector("#adpTotal"),
      rowTotal: document.querySelector("#rowTotal"),
      autofillCount: document.querySelector("#autofillCount"),
      autofillAdpCount: document.querySelector("#autofillAdpCount"),
      autofillIcd9Count: document.querySelector("#autofillIcd9Count"),
      referCleanupCount: document.querySelector("#referCleanupCount"),
      autofillStatus: document.querySelector("#autofillStatus"),
      referCleanupStatus: document.querySelector("#referCleanupStatus"),
      fileTable: document.querySelector("#fileTable"),
      autofillTable: document.querySelector("#autofillTable"),
      referCleanupTable: document.querySelector("#referCleanupTable"),
      caseTable: document.querySelector("#caseTable")
    };

    function baseName(file) {
      const raw = file.webkitRelativePath || file.name;
      const name = raw.split(/[\\/]/).pop() || raw;
      return name.replace(/\.[^.]+$/, "").toUpperCase();
    }

    function decodeLine(bytes) {
      return asciiDecoder.decode(bytes);
    }

    function splitLines(bytes) {
      const lines = [];
      let start = 0;
      let newline = new Uint8Array([13, 10]);
      for (let i = 0; i < bytes.length; i += 1) {
        if (bytes[i] !== 10) continue;
        let end = i;
        if (end > start && bytes[end - 1] === 13) end -= 1;
        lines.push(bytes.slice(start, end));
        start = i + 1;
      }
      if (start < bytes.length) lines.push(bytes.slice(start));
      if (!bytes.includes(13)) newline = new Uint8Array([10]);
      if (lines.length === 0) lines.push(new Uint8Array());
      return { lines, newline };
    }

    function selectedValues(line, indexes) {
      const values = {};
      if (!indexes.size) return values;
      const text = decodeLine(line);
      const parts = text.split("|");
      for (const [field, index] of indexes) values[field] = (parts[index] || "").trim();
      return values;
    }

    async function parseFile(file) {
      const name = file.baseName || baseName(file);
      const bytes = file.bytes instanceof Uint8Array ? file.bytes : new Uint8Array(await file.arrayBuffer());
      const { lines, newline } = splitLines(bytes);
      const headers = decodeLine(lines[0] || new Uint8Array()).replace(/^\uFEFF/, "").replace(/^/, "").split("|");
      const index = new Map(headers.map((h, i) => [h.trim().toUpperCase(), i]));
      const wantedIndexes = new Map();
      for (const field of FIELD_NAMES) {
        if (index.has(field)) wantedIndexes.set(field, index.get(field));
      }
      const rows = [];
      for (let i = 1; i < lines.length; i += 1) {
        if (lines[i].length === 0) continue;
        rows.push({
          line: lines[i],
          rowNumber: i + 1,
          values: selectedValues(lines[i], wantedIndexes)
        });
      }
      return {
        name,
        originalName: file.entryName || `${name}.txt`,
        bytes,
        newline,
        headerLine: lines[0] || new Uint8Array(),
        headers,
        index,
        rows,
        filteredRows: [],
        note: ""
      };
    }

    async function collectTextFiles(files) {
      const output = [];
      const warnings = [];
      for (const file of Array.from(files || [])) {
        if (/\.txt$/i.test(file.name)) {
          output.push(file);
          continue;
        }
        if (/\.zip$/i.test(file.name)) {
          try {
            const entries = await readZipTextEntries(file);
            output.push(...entries);
          } catch (error) {
            warnings.push(`${file.name}: ${error.message}`);
          }
        }
      }
      return { output, warnings };
    }

    async function loadFiles(files) {
      setBusy(true, "กำลังอ่านไฟล์...");
      clearAlert();
      const sourceCount = Array.from(files || []).length;
      const { output: list, warnings } = await collectTextFiles(files);
      if (!list.length) {
        setBusy(false, "พร้อมใช้งาน");
        const detail = warnings.length ? ` (${warnings.join("; ")})` : "";
        showAlert(`ไม่พบไฟล์ .txt หรืออ่าน .zip ไม่ได้ กรุณาเลือกชุด 16 แฟ้ม${detail}`);
        return;
      }

      const parsed = await Promise.all(list.map(parseFile));
      state.tables = new Map();
      state.autofillEnabled = false;
      state.referCleanupEnabled = false;
      for (const table of parsed) {
        if (!EXPECTED_FILES.includes(table.name)) continue;
        state.tables.set(table.name, table);
      }

      analyze();
      setBusy(false, "วิเคราะห์แล้ว");
      el.uploadStatus.textContent = `อ่านจากรายการอัปโหลด ${sourceCount.toLocaleString("th-TH")} รายการ, พบ .txt ${list.length.toLocaleString("th-TH")} ไฟล์, ใช้ในชุด 16 แฟ้ม ${state.tables.size.toLocaleString("th-TH")} ไฟล์`;
      if (warnings.length) showAlert(`บางไฟล์ zip อ่านไม่ได้: ${warnings.join("; ")}`);
    }

    function visitKey(row) {
      const an = clean(row.AN);
      const seq = clean(row.SEQ);
      if (an) return `AN:${an}`;
      if (seq) return `SEQ:${seq}`;
      return "";
    }

    function clean(value) {
      return String(value || "").trim();
    }

    function normalizeType(value) {
      const text = clean(value);
      const numeric = Number(text);
      return Number.isFinite(numeric) ? String(numeric) : text;
    }

    function normalizeCode(value) {
      return clean(value).toUpperCase();
    }

    function normalizeDiag(value) {
      return clean(value).toUpperCase().replace(/\./g, "");
    }

    function isTargetAdp(row) {
      const type = normalizeType(row.TYPE);
      const code = normalizeCode(row.CODE);
      return type === TARGET_ADP_TYPE && code === TARGET_ADP_CODE;
    }

    function adpReason(row) {
      const type = normalizeType(row.TYPE);
      const code = normalizeCode(row.CODE);
      return `ADP ${type}|${code}`;
    }

    function isTargetOdx(row) {
      return normalizeDiag(row.DIAG) === TARGET_ODX_DIAG;
    }

    function procedureCode(row) {
      return normalizeCode(firstValue(row.OPER, row.ICD9, row.PROCED, row.CODE)).replace(/\./g, "");
    }

    function isTargetProcedure(row) {
      return TARGET_ICD9_CODES.has(procedureCode(row));
    }

    function toCents(value) {
      const text = clean(value).replace(/,/g, "");
      if (!text) return 0;
      const negative = text.startsWith("-");
      const normalized = negative ? text.slice(1) : text;
      const [whole, fraction = ""] = normalized.split(".");
      const cents = (parseInt(whole || "0", 10) * 100) + parseInt((fraction + "00").slice(0, 2), 10);
      return Number.isFinite(cents) ? (negative ? -cents : cents) : 0;
    }

    function parseFdhDate(value) {
      const text = clean(value).replace(/\D/g, "");
      if (text.length !== 8) return null;
      const year = Number(text.slice(0, 4));
      const month = Number(text.slice(4, 6));
      const day = Number(text.slice(6, 8));
      if (!year || !month || !day) return null;
      const date = new Date(year, month - 1, day);
      if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
      return date;
    }

    function ageOnDate(dobValue, serviceDateValue) {
      const dob = parseFdhDate(dobValue);
      const service = parseFdhDate(serviceDateValue);
      if (!dob || !service) return null;
      let age = service.getFullYear() - dob.getFullYear();
      const beforeBirthday = service.getMonth() < dob.getMonth()
        || (service.getMonth() === dob.getMonth() && service.getDate() < dob.getDate());
      if (beforeBirthday) age -= 1;
      return age;
    }

    function inTargetAge(age) {
      return age !== null && age >= MIN_AGE && age <= MAX_AGE;
    }

    function normalizeNation(value) {
      const text = clean(value).replace(/\D/g, "");
      return text.padStart(3, "0");
    }

    function addReason(caseMap, key, row, reason) {
      if (!key) return;
      const found = caseMap.get(key) || {
        key,
        type: key.startsWith("AN:") ? "IPD" : "OPD",
        hn: clean(row.HN),
        an: clean(row.AN),
        seq: clean(row.SEQ),
        date: firstValue(row.DATEDX, row.DATEOPD, row.DATE, row.DATEADM, row.DATEIN, row.DATEOUT),
        personIds: new Set(),
        reasons: new Set(),
        chtCents: 0,
        age: "",
        serviceDate: "",
        adpCents: 0,
        hasK020: false,
        hasAdp15001: false,
        icd9Codes: new Set(),
        icd9Ref: null,
        odxRef: null
      };
      if (clean(row.HN)) found.hn = clean(row.HN);
      if (clean(row.AN)) found.an = clean(row.AN);
      if (clean(row.SEQ)) found.seq = clean(row.SEQ);
      if (firstValue(row.DATEDX, row.DATEOPD, row.DATE, row.DATEADM, row.DATEIN, row.DATEOUT)) found.date = firstValue(row.DATEDX, row.DATEOPD, row.DATE, row.DATEADM, row.DATEIN, row.DATEOUT);
      if (clean(row.PERSON_ID)) found.personIds.add(clean(row.PERSON_ID));
      found.reasons.add(reason);
      caseMap.set(key, found);
    }

    function firstValue(...values) {
      for (const value of values) {
        const text = clean(value);
        if (text) return text;
      }
      return "";
    }

    function analyze() {
      const candidates = new Map();
      const patientByHn = new Map();
      const patientByPerson = new Map();
      const serviceDateByKey = new Map();
      const clinicByKey = new Map();
      const visitInfoByKey = new Map();
      const odxK020Keys = new Set();
      const adp15001Keys = new Set();
      const icd9CodesByKey = new Map();
      const icd9RefByKey = new Map();
      const ucsKeys = new Set();
      const ucsHn = new Set();
      const ucsPerson = new Set();
      const odxRefByKey = new Map();
      const caseMap = new Map();
      const matchedKeys = new Set();
      const matchedHn = new Set();
      const matchedAn = new Set();
      const matchedSeq = new Set();
      const matchedPerson = new Set();
      let adpCents = 0;

      const rememberServiceDate = (key, value) => {
        if (!key || serviceDateByKey.has(key) || !clean(value)) return;
        serviceDateByKey.set(key, clean(value));
      };
      const rememberClinic = (key, value) => {
        if (!key || clinicByKey.has(key) || !clean(value)) return;
        clinicByKey.set(key, clean(value));
      };
      const rememberVisitInfo = (key, row) => {
        if (!key) return;
        const found = visitInfoByKey.get(key) || { HN: "", AN: "", SEQ: "", PERSON_ID: "" };
        for (const field of ["HN", "AN", "SEQ", "PERSON_ID"]) {
          if (!found[field] && clean(row[field])) found[field] = clean(row[field]);
        }
        visitInfoByKey.set(key, found);
      };

      const rememberIcd9 = (key, row) => {
        if (!key) return;
        const code = procedureCode(row);
        if (!TARGET_ICD9_CODES.has(code)) return;
        if (!icd9CodesByKey.has(key)) icd9CodesByKey.set(key, new Set());
        icd9CodesByKey.get(key).add(code);
        if (!icd9RefByKey.has(key)) icd9RefByKey.set(key, row);
      };

      const pat = state.tables.get("PAT");
      if (pat) {
        for (const row of pat.rows) {
          const hn = clean(row.values.HN);
          const personId = clean(row.values.PERSON_ID);
          const patient = {
            hn,
            personId,
            dob: clean(row.values.DOB),
            sex: normalizeType(row.values.SEX),
            nation: normalizeNation(row.values.NATION)
          };
          if (hn) patientByHn.set(hn, patient);
          if (personId) patientByPerson.set(personId, patient);
        }
      }

      for (const name of ["ADP", "OPD", "IPD", "CHT", "ODX", "OOP", "IOP"]) {
        const table = state.tables.get(name);
        if (!table) continue;
        for (const row of table.rows) {
          const key = visitKey(row.values);
          rememberServiceDate(key, firstValue(row.values.DATEOPD, row.values.DATEADM, row.values.DATE, row.values.DATEDX, row.values.DATEIN, row.values.DATEOUT));
          rememberClinic(key, row.values.CLINIC);
          rememberVisitInfo(key, row.values);
        }
      }

      const ins = state.tables.get("INS");
      if (ins) {
        for (const row of ins.rows) {
          if (normalizeCode(row.values.INSCL) !== "UCS") continue;
          const key = visitKey(row.values);
          if (key) ucsKeys.add(key);
          if (clean(row.values.HN)) ucsHn.add(clean(row.values.HN));
          if (clean(row.values.PERSON_ID)) ucsPerson.add(clean(row.values.PERSON_ID));
          rememberVisitInfo(key, row.values);
        }
      }

      const odx = state.tables.get("ODX");
      if (odx) {
        for (const row of odx.rows) {
          const key = visitKey(row.values);
          if (!key) continue;
          if (!odxRefByKey.has(key)) odxRefByKey.set(key, row.values);
          const diag = normalizeDiag(row.values.DIAG);
          if (diag === AUTOFILL_ODX_DIAG) odxK020Keys.add(key);
          if (isTargetOdx(row.values)) {
            const found = candidates.get(key) || {
              key,
              source: row.values,
              hn: clean(row.values.HN),
              an: clean(row.values.AN),
              seq: clean(row.values.SEQ),
              personId: clean(row.values.PERSON_ID),
              adpCents: 0,
              serviceDate: clean(row.values.DATEDX),
              codes: new Set()
            };
            found.codes.add(`ODX ${TARGET_ODX_DIAG}`);
            if (clean(row.values.HN)) found.hn = clean(row.values.HN);
            if (clean(row.values.SEQ)) found.seq = clean(row.values.SEQ);
            if (clean(row.values.PERSON_ID)) found.personId = clean(row.values.PERSON_ID);
            if (clean(row.values.DATEDX)) found.serviceDate = clean(row.values.DATEDX);
            candidates.set(key, found);
          }
        }
      }

      const adp = state.tables.get("ADP");
      if (adp) {
        for (const row of adp.rows) {
          const key = visitKey(row.values);
          const type = normalizeType(row.values.TYPE);
          const code = normalizeCode(row.values.CODE);
          if (type === AUTOFILL_ADP_TYPE && code === AUTOFILL_ADP_CODE && key) adp15001Keys.add(key);
          if (isTargetAdp(row.values)) {
            if (!key) continue;
            const found = candidates.get(key) || {
              key,
              source: row.values,
              hn: clean(row.values.HN),
              an: clean(row.values.AN),
              seq: clean(row.values.SEQ),
              personId: clean(row.values.PERSON_ID),
              adpCents: 0,
              serviceDate: clean(row.values.DATEOPD),
              codes: new Set()
            };
            found.codes.add(adpReason(row.values));
            found.adpCents += toCents(row.values.TOTAL);
            if (clean(row.values.HN)) found.hn = clean(row.values.HN);
            if (clean(row.values.AN)) found.an = clean(row.values.AN);
            if (clean(row.values.SEQ)) found.seq = clean(row.values.SEQ);
            if (clean(row.values.PERSON_ID)) found.personId = clean(row.values.PERSON_ID);
            if (clean(row.values.DATEOPD)) found.serviceDate = clean(row.values.DATEOPD);
            candidates.set(key, found);
          }
        }
      }

      for (const name of ["OOP", "IOP"]) {
        const table = state.tables.get(name);
        if (!table) continue;
        for (const row of table.rows) {
          rememberIcd9(visitKey(row.values), row.values);
          if (!isTargetProcedure(row.values)) continue;
          const key = visitKey(row.values);
          if (!key) continue;
          const found = candidates.get(key) || {
            key,
            source: row.values,
            hn: clean(row.values.HN),
            an: clean(row.values.AN),
            seq: clean(row.values.SEQ),
            personId: clean(row.values.PERSON_ID),
            adpCents: 0,
            serviceDate: firstValue(row.values.DATEOPD, row.values.DATEIN, row.values.DATEOUT),
            codes: new Set()
          };
          found.codes.add(`${name} ICD9 ${procedureCode(row.values)}`);
          if (clean(row.values.HN)) found.hn = clean(row.values.HN);
          if (clean(row.values.AN)) found.an = clean(row.values.AN);
          if (clean(row.values.SEQ)) found.seq = clean(row.values.SEQ);
          if (clean(row.values.PERSON_ID)) found.personId = clean(row.values.PERSON_ID);
          if (firstValue(row.values.DATEOPD, row.values.DATEIN, row.values.DATEOUT)) found.serviceDate = firstValue(row.values.DATEOPD, row.values.DATEIN, row.values.DATEOUT);
          candidates.set(key, found);
        }
      }

      for (const candidate of candidates.values()) {
        const info = visitInfoByKey.get(candidate.key) || {};
        const hn = candidate.hn || info.HN || "";
        const personId = candidate.personId || info.PERSON_ID || "";
        if (!ucsKeys.has(candidate.key)) continue;
        const patient = (hn && patientByHn.get(hn)) || (personId && patientByPerson.get(personId)) || null;
        if (!patient) continue;
        const serviceDate = candidate.serviceDate || serviceDateByKey.get(candidate.key);
        const age = ageOnDate(patient.dob, serviceDate);
        if (!inTargetAge(age)) continue;

        const seed = {
          HN: hn || patient.hn,
          AN: candidate.an || info.AN,
          SEQ: candidate.seq || info.SEQ,
          PERSON_ID: patient.personId
        };
        addReason(caseMap, candidate.key, seed, Array.from(candidate.codes).sort().join(", "));
        addReason(caseMap, candidate.key, seed, `อายุ ${age} ปี`);
        const item = caseMap.get(candidate.key);
        item.adpCents = candidate.adpCents;
        item.serviceDate = serviceDate || "";
        item.date = serviceDate || item.date;
        item.age = age;
        item.clinic = clinicByKey.get(candidate.key) || "";
        item.hasK020 = odxK020Keys.has(candidate.key);
        item.hasAdp15001 = adp15001Keys.has(candidate.key);
        item.icd9Codes = icd9CodesByKey.get(candidate.key) || new Set();
        item.icd9Ref = icd9RefByKey.get(candidate.key) || null;
        item.odxRef = odxRefByKey.get(candidate.key) || null;
        item.adpRef = candidate.source || null;
        adpCents += candidate.adpCents;
        matchedKeys.add(candidate.key);
      }

      enrichCasesFromTable(caseMap, "OPD", ["DATEOPD"]);
      enrichCasesFromTable(caseMap, "IPD", ["DATEADM"]);
      enrichCasesFromTable(caseMap, "INS", []);

      for (const item of caseMap.values()) {
        const patient = (item.hn && patientByHn.get(item.hn)) || null;
        if (patient?.personId) item.personIds.add(patient.personId);
        if (!item.serviceDate) item.serviceDate = item.date;
        if (!item.age && patient) {
          const age = ageOnDate(patient.dob, item.serviceDate || item.date);
          if (age !== null) item.age = age;
        }
      }

      const cht = state.tables.get("CHT");
      if (cht) {
        for (const row of cht.rows) {
          const key = visitKey(row.values);
          if (!caseMap.has(key)) continue;
          const item = caseMap.get(key);
          item.chtCents += toCents(row.values.TOTAL);
          if (clean(row.values.HN)) item.hn = clean(row.values.HN);
          if (clean(row.values.AN)) item.an = clean(row.values.AN);
          if (clean(row.values.SEQ)) item.seq = clean(row.values.SEQ);
          if (clean(row.values.DATE)) item.date = clean(row.values.DATE);
          if (clean(row.values.PERSON_ID)) item.personIds.add(clean(row.values.PERSON_ID));
        }
      }

      for (const item of caseMap.values()) {
        if (item.hn) matchedHn.add(item.hn);
        if (item.an) matchedAn.add(item.an);
        if (item.seq) matchedSeq.add(item.seq);
        for (const personId of item.personIds) matchedPerson.add(personId);
      }

      for (const table of state.tables.values()) {
        table.filteredRows = table.rows.filter(row => includeRow(table.name, row.values, matchedKeys, matchedHn, matchedAn, matchedSeq, matchedPerson));
      }

      const cases = Array.from(caseMap.values()).sort((a, b) => a.key.localeCompare(b.key));
      const autofillOdxCases = cases.filter(item => !item.hasK020);
      const autofillAdpCases = cases.filter(item => !item.hasAdp15001);
      const autofillIcd9Rows = cases.filter(item => item.hasAdp15001).flatMap(item => makeAutofillIcd9Rows(item)).filter(Boolean);
      const autofillOdxRows = autofillOdxCases.map(item => makeAutofillOdxRow(item)).filter(Boolean);
      const autofillAdpRows = autofillAdpCases.map(item => makeAutofillAdp15001Row(item)).filter(Boolean);
      const autofillIcd9Cases = cases.filter(item => item.hasAdp15001 && AUTOFILL_ICD9_CODES.some(code => !(item.icd9Codes || new Set()).has(code)));
      const autofillCases = Array.from(new Set([...autofillOdxCases, ...autofillAdpCases, ...autofillIcd9Cases]));
      const autofillRows = [...autofillOdxRows, ...autofillAdpRows, ...autofillIcd9Rows];
      const referCleanup = analyzeReferCleanup();
      for (const item of cases) item.referDeductionCents = referCleanup.chaDeductions.get(item.key) || 0;
      const chtCents = cases.length * CASE_RATE_CENTS;
      const baseRowTotal = Array.from(state.tables.values()).reduce((sum, table) => sum + table.filteredRows.length, 0);
      const rowTotal = baseRowTotal - (state.referCleanupEnabled ? referCleanup.removedRows : 0) + (state.autofillEnabled ? autofillRows.length : 0);
      const missing = EXPECTED_FILES.filter(name => !state.tables.has(name));

      state.result = {
        cases,
        autofillCases,
        autofillOdxCases,
        autofillAdpCases,
        autofillIcd9Cases,
        autofillOdxRows,
        autofillAdpRows,
        autofillIcd9Rows,
        autofillRows,
        referCleanup,
        adpCents,
        chtCents,
        rowTotal,
        missing
      };

      render();
    }

    function analyzeReferCleanup() {
      const cleanup = {
        adpRows: 0,
        chaRows: 0,
        chtRows: 0,
        chtCents: 0,
        orfRows: 0,
        removedRows: 0,
        chaDeductions: new Map(),
        previewRows: []
      };

      const addPreview = (row, file, code, detail, total) => {
        cleanup.previewRows.push({
          HN: clean(row.HN),
          FILE: file,
          DATE: firstValue(row.DATEOPD, row.DATEADM, row.DATE),
          CODE: code,
          DETAIL: detail,
          SEQ: clean(row.SEQ),
          TOTAL: total
        });
      };

      const orf = state.tables.get("ORF");
      if (orf) {
        cleanup.orfRows = orf.filteredRows.length;
        cleanup.removedRows += cleanup.orfRows;
        for (const row of orf.filteredRows.slice(0, 50)) {
          addPreview(row.values, "ORF", "ALL", "ลบแถว ORF เหลือ header", "");
        }
      }

      const adp = state.tables.get("ADP");
      if (adp) {
        for (const row of adp.filteredRows) {
          if (!isReferAdpRow(row.values)) continue;
          cleanup.adpRows += 1;
          cleanup.removedRows += 1;
          addPreview(row.values, "ADP", typeCodeKey(row.values), "ลบ ADP refer", clean(row.values.TOTAL));
        }
      }

      const cha = state.tables.get("CHA");
      if (cha) {
        for (const row of cha.filteredRows) {
          if (!isReferChaRow(row.values)) continue;
          const key = visitKey(row.values);
          const amount = toCents(row.values.AMOUNT);
          cleanup.chaRows += 1;
          cleanup.removedRows += 1;
          cleanup.chtCents += amount;
          cleanup.chaDeductions.set(key, (cleanup.chaDeductions.get(key) || 0) + amount);
          addPreview(row.values, "CHA", `${clean(row.values.CHRGITEM)}|${clean(row.values.AMOUNT)}`, "ลบ CHA refer และหัก CHT.TOTAL", clean(row.values.AMOUNT));
        }
      }

      const cht = state.tables.get("CHT");
      if (cht) {
        for (const row of cht.filteredRows) {
          const deduction = cleanup.chaDeductions.get(visitKey(row.values)) || 0;
          if (!deduction) continue;
          cleanup.chtRows += 1;
          addPreview(row.values, "CHT", "TOTAL", `หัก ${formatMoney(deduction)} บาท`, `-${formatMoney(deduction)}`);
        }
      }

      return cleanup;
    }

    function includeRow(fileName, row, matchedKeys, matchedHn, matchedAn, matchedSeq, matchedPerson) {
      if (!matchedKeys.size) return false;
      const key = visitKey(row);
      const hn = clean(row.HN);
      const an = clean(row.AN);
      const seq = clean(row.SEQ);
      const personId = clean(row.PERSON_ID);

      if (fileName === "INS") {
        if (normalizeCode(row.INSCL) !== "UCS") return false;
        return (key && matchedKeys.has(key)) || (an && matchedAn.has(an)) || (seq && matchedSeq.has(seq)) || (hn && matchedHn.has(hn)) || (personId && matchedPerson.has(personId));
      }

      if (key && matchedKeys.has(key)) return true;

      if (an && matchedAn.has(an)) return true;
      if (seq && matchedSeq.has(seq)) return true;

      if (fileName === "PAT") {
        return (hn && matchedHn.has(hn)) || (personId && matchedPerson.has(personId));
      }

      return false;
    }

    function enrichCasesFromTable(caseMap, fileName, dateFields) {
      const table = state.tables.get(fileName);
      if (!table) return;
      for (const row of table.rows) {
        const key = visitKey(row.values);
        if (!caseMap.has(key)) continue;
        const item = caseMap.get(key);
        if (clean(row.values.HN)) item.hn = clean(row.values.HN);
        if (clean(row.values.AN)) item.an = clean(row.values.AN);
        if (clean(row.values.SEQ)) item.seq = clean(row.values.SEQ);
        if (clean(row.values.PERSON_ID)) item.personIds.add(clean(row.values.PERSON_ID));
        for (const field of dateFields) {
          if (clean(row.values[field])) {
            item.date = clean(row.values[field]);
            break;
          }
        }
      }
    }

    function typeCodeKey(row) {
      return `${normalizeType(row.TYPE)}|${normalizeCode(row.CODE)}`;
    }

    function isReferAdpRow(row) {
      return REFER_ADP_REMOVE.has(typeCodeKey(row));
    }

    function isReferChaRow(row) {
      return normalizeCode(row.CHRGITEM) === REFER_CHA_CHRGITEM && toCents(row.AMOUNT) === REFER_CHA_AMOUNT_CENTS;
    }

    function centsToField(cents) {
      return (cents / 100).toFixed(2);
    }

    function rowLineWithOverrides(table, row, overrides) {
      const parts = decodeLine(row.line).split("|");
      for (const [field, value] of Object.entries(overrides)) {
        const index = table.index.get(field);
        if (index !== undefined) parts[index] = clean(value);
      }
      return new TextEncoder().encode(parts.join("|"));
    }

    function makeAutofillOdxRow(item) {
      const odx = state.tables.get("ODX");
      if (!odx) return null;
      const values = new Array(odx.headers.length).fill("");
      const ref = item.odxRef || {};
      const set = (field, value) => {
        const index = odx.index.get(field);
        if (index !== undefined) values[index] = clean(value);
      };

      set("HN", item.hn || ref.HN);
      set("DATEDX", item.serviceDate || item.date || ref.DATEDX);
      set("CLINIC", item.clinic || ref.CLINIC || "000");
      set("DIAG", AUTOFILL_ODX_DIAG);
      set("DXTYPE", ref.DXTYPE || "1");
      set("DRDX", ref.DRDX || "");
      set("PERSON_ID", Array.from(item.personIds || [])[0] || ref.PERSON_ID || "");
      set("SEQ", item.seq || ref.SEQ);

      return {
        generated: true,
        line: new TextEncoder().encode(values.join("|")),
        rowNumber: 0,
        values: {
          HN: values[odx.index.get("HN")] || "",
          FILE: "ODX",
          SEQ: values[odx.index.get("SEQ")] || "",
          DATEDX: values[odx.index.get("DATEDX")] || "",
          DATE: values[odx.index.get("DATEDX")] || "",
          DIAG: AUTOFILL_ODX_DIAG,
          CODE: AUTOFILL_ODX_DIAG,
          DXTYPE: values[odx.index.get("DXTYPE")] || "",
          CLINIC: values[odx.index.get("CLINIC")] || "",
          DETAIL: `DXTYPE=${values[odx.index.get("DXTYPE")] || ""} CLINIC=${values[odx.index.get("CLINIC")] || ""}`,
          TOTAL: "",
          FIELDS: `HN=${values[odx.index.get("HN")] || ""} | DATEDX=${values[odx.index.get("DATEDX")] || ""} | DIAG=${AUTOFILL_ODX_DIAG} | SEQ=${values[odx.index.get("SEQ")] || ""}`
        }
      };
    }

    function makeAutofillAdp15001Row(item) {
      const adp = state.tables.get("ADP");
      if (!adp) return null;
      const values = new Array(adp.headers.length).fill("");
      const ref = item.adpRef || {};
      const set = (field, value) => {
        const index = adp.index.get(field);
        if (index !== undefined) values[index] = clean(value);
      };

      set("HN", item.hn || ref.HN);
      set("AN", item.an || ref.AN);
      set("DATEOPD", item.serviceDate || item.date || ref.DATEOPD);
      set("TYPE", AUTOFILL_ADP_TYPE);
      set("CODE", AUTOFILL_ADP_CODE);
      set("QTY", "1");
      set("RATE", AUTOFILL_ADP_RATE);
      set("SEQ", item.seq || ref.SEQ);
      set("TOTCOPAY", "0.00");
      set("TOTAL", AUTOFILL_ADP_RATE);
      set("BI", ref.BI || "000");
      set("CLINIC", item.clinic || ref.CLINIC || "000");
      set("ITEMSRC", ref.ITEMSRC || "2");
      set("PROVIDER", ref.PROVIDER || "");

      return {
        generated: true,
        line: new TextEncoder().encode(values.join("|")),
        rowNumber: 0,
        values: {
          HN: values[adp.index.get("HN")] || "",
          FILE: "ADP",
          SEQ: values[adp.index.get("SEQ")] || "",
          DATE: values[adp.index.get("DATEOPD")] || "",
          CODE: `${AUTOFILL_ADP_TYPE}|${AUTOFILL_ADP_CODE}`,
          DETAIL: `QTY=1 RATE=${AUTOFILL_ADP_RATE}`,
          TOTAL: AUTOFILL_ADP_RATE,
          FIELDS: `HN=${values[adp.index.get("HN")] || ""} | DATEOPD=${values[adp.index.get("DATEOPD")] || ""} | TYPE=${AUTOFILL_ADP_TYPE} | CODE=${AUTOFILL_ADP_CODE} | SEQ=${values[adp.index.get("SEQ")] || ""}`
        }
      };
    }

    function makeAutofillIcd9Rows(item) {
      const existing = item.icd9Codes || new Set();
      const missingCodes = AUTOFILL_ICD9_CODES.filter(code => !existing.has(code));
      if (!missingCodes.length) return [];
      const tableName = item.an ? "IOP" : "OOP";
      const table = state.tables.get(tableName);
      if (!table) return [];
      const ref = item.icd9Ref || item.adpRef || item.odxRef || {};

      return missingCodes.map(code => {
        const values = new Array(table.headers.length).fill("");
        const set = (field, value) => {
          const index = table.index.get(field);
          if (index !== undefined) values[index] = clean(value);
        };

        if (tableName === "IOP") {
          set("AN", item.an || ref.AN);
          set("OPER", code);
          set("OPTYPE", ref.OPTYPE || "1");
          set("DROPID", ref.DROPID || "");
          set("DATEIN", item.serviceDate || item.date || ref.DATEIN || ref.DATEOPD || ref.DATEDX);
          set("TIMEIN", ref.TIMEIN || "");
          set("DATEOUT", item.serviceDate || item.date || ref.DATEOUT || ref.DATEOPD || ref.DATEDX);
          set("TIMEOUT", ref.TIMEOUT || "");
        } else {
          set("HN", item.hn || ref.HN);
          set("DATEOPD", item.serviceDate || item.date || ref.DATEOPD || ref.DATEDX);
          set("CLINIC", item.clinic || ref.CLINIC || "000");
          set("OPER", code);
          set("DROPID", ref.DROPID || "");
          set("PERSON_ID", Array.from(item.personIds || [])[0] || ref.PERSON_ID || "");
          set("SEQ", item.seq || ref.SEQ);
          set("SERVPRICE", ref.SERVPRICE || "");
        }

        return {
          generated: true,
          line: new TextEncoder().encode(values.join("|")),
          rowNumber: 0,
          values: {
            HN: item.hn || values[table.index.get("HN")] || "",
            FILE: tableName,
            SEQ: item.seq || values[table.index.get("SEQ")] || "",
            DATE: values[table.index.get("DATEOPD")] || values[table.index.get("DATEIN")] || "",
            CODE: code,
            DETAIL: tableName === "IOP" ? `AN=${values[table.index.get("AN")] || ""} OPTYPE=${values[table.index.get("OPTYPE")] || ""}` : `OPER=${code} CLINIC=${values[table.index.get("CLINIC")] || ""}`,
            TOTAL: "",
            FIELDS: tableName === "IOP"
              ? `AN=${values[table.index.get("AN")] || ""} | OPER=${code} | DATEIN=${values[table.index.get("DATEIN")] || ""}`
              : `HN=${values[table.index.get("HN")] || ""} | DATEOPD=${values[table.index.get("DATEOPD")] || ""} | OPER=${code} | SEQ=${values[table.index.get("SEQ")] || ""}`
          }
        };
      });
    }

    function render() {
      const result = state.result || { cases: [], autofillCases: [], autofillOdxCases: [], autofillAdpCases: [], autofillIcd9Cases: [], autofillOdxRows: [], autofillAdpRows: [], autofillIcd9Rows: [], autofillRows: [], referCleanup: { adpRows: 0, chaRows: 0, chtRows: 0, chtCents: 0, orfRows: 0, removedRows: 0, previewRows: [] }, adpCents: 0, chtCents: 0, rowTotal: 0, missing: EXPECTED_FILES };
      el.caseCount.textContent = result.cases.length.toLocaleString("th-TH");
      el.chtTotal.textContent = formatMoney(result.chtCents);
      el.adpTotal.textContent = formatMoney(result.adpCents);
      el.rowTotal.textContent = result.rowTotal.toLocaleString("th-TH");
      el.autofillCount.textContent = result.autofillOdxCases.length.toLocaleString("th-TH");
      el.autofillAdpCount.textContent = result.autofillAdpCases.length.toLocaleString("th-TH");
      el.autofillIcd9Count.textContent = result.autofillIcd9Rows.length.toLocaleString("th-TH");
      el.referCleanupCount.textContent = result.referCleanup.previewRows.length.toLocaleString("th-TH");
      el.exportButton.disabled = !result.cases.length;
      el.autofillButton.disabled = !result.autofillCases.length || state.autofillEnabled || !state.tables.has("ODX") || !state.tables.has("ADP");
      el.referCleanupButton.disabled = !result.referCleanup.previewRows.length || state.referCleanupEnabled;
      el.autofillStatus.textContent = state.autofillEnabled
        ? `สำเร็จ: เติม ODX ${result.autofillOdxRows.length.toLocaleString("th-TH")} แถว, ADP ${result.autofillAdpRows.length.toLocaleString("th-TH")} แถว และ ICD9 ${result.autofillIcd9Rows.length.toLocaleString("th-TH")} แถว รอ export`
        : (result.autofillCases.length ? `พบรายการเติมได้: ODX ${result.autofillOdxRows.length.toLocaleString("th-TH")} แถว + ADP ${result.autofillAdpRows.length.toLocaleString("th-TH")} แถว + ICD9 ${result.autofillIcd9Rows.length.toLocaleString("th-TH")} แถว` : "ยังไม่มีรายการที่ต้องเติม");
      el.referCleanupStatus.textContent = state.referCleanupEnabled
        ? `สำเร็จ: ลบ refer ${result.referCleanup.removedRows.toLocaleString("th-TH")} แถว และหัก CHT ${formatMoney(result.referCleanup.chtCents)} บาท รอ export`
        : (result.referCleanup.previewRows.length ? `พบรายการ refer ${result.referCleanup.previewRows.length.toLocaleString("th-TH")} รายการ กดปุ่มเพื่อลบและหักยอดตอน export` : "ยังไม่พบรายการ refer ที่ต้องลบ");

      el.fileTable.innerHTML = EXPECTED_FILES.map(name => {
        const table = state.tables.get(name);
        if (!table) {
          return `<tr><td>${name}.txt</td><td class="bad">ไม่พบ</td><td>0</td><td>0</td><td>จะ export เป็นแฟ้ม header ว่างไม่ได้ กรุณาอัปโหลดให้ครบ</td></tr>`;
        }
        const extraRows = state.autofillEnabled
          ? ((name === "ODX" ? result.autofillOdxRows.length : 0) + (name === "ADP" ? result.autofillAdpRows.length : 0) + (name === "OOP" ? result.autofillIcd9Rows.filter(row => row.values.FILE === "OOP").length : 0) + (name === "IOP" ? result.autofillIcd9Rows.filter(row => row.values.FILE === "IOP").length : 0))
          : 0;
        const removedRows = state.referCleanupEnabled
          ? (name === "ORF"
            ? result.referCleanup.orfRows
            : (name === "ADP" ? result.referCleanup.adpRows : (name === "CHA" ? result.referCleanup.chaRows : 0)))
          : 0;
        const filteredCount = table.filteredRows.length - removedRows + extraRows;
        const notes = [];
        if (extraRows) notes.push(`Autofill เพิ่ม ${extraRows.toLocaleString("th-TH")} แถว`);
        if (removedRows) notes.push(`ลบ refer ${removedRows.toLocaleString("th-TH")} แถว`);
        if (name === "CHT" && state.referCleanupEnabled && result.referCleanup.chtRows) notes.push(`หัก TOTAL ${formatMoney(result.referCleanup.chtCents)} บาท`);
        const note = notes.length ? `พร้อม export, ${notes.join(", ")}` : (table.filteredRows.length ? "พร้อม export" : "ไม่มีแถวเข้าเงื่อนไข");
        return `<tr><td>${name}.txt</td><td class="ok">พบ</td><td>${table.rows.length.toLocaleString("th-TH")}</td><td>${filteredCount.toLocaleString("th-TH")}</td><td>${note}</td></tr>`;
      }).join("");

      renderAutofillTable(result);
      renderReferCleanupTable(result);

      const preview = result.cases.slice(0, 200);
      el.caseTable.innerHTML = preview.length ? preview.map((item, index) => {
        return `<tr>
          <td>${(index + 1).toLocaleString("th-TH")}</td>
          <td>${escapeHtml(item.type)}</td>
          <td>${escapeHtml(item.hn)}</td>
          <td>${escapeHtml(item.an)}</td>
          <td>${escapeHtml(item.seq)}</td>
          <td>${escapeHtml(item.age)}</td>
          <td>${escapeHtml(Array.from(item.reasons).join(", "))}</td>
          <td>${formatMoney(CASE_RATE_CENTS)}</td>
        </tr>`;
      }).join("") : `<tr><td colspan="8" class="muted">ยังไม่มีเคสเข้าเงื่อนไข</td></tr>`;

      if (result.cases.length > preview.length) {
        el.caseTable.insertAdjacentHTML("beforeend", `<tr><td colspan="8" class="muted">แสดง 200 เคสแรกจากทั้งหมด ${result.cases.length.toLocaleString("th-TH")} เคส</td></tr>`);
      }

      if (result.missing.length && state.tables.size) {
        showAlert(`ยังขาดแฟ้ม: ${result.missing.map(name => `${name}.txt`).join(", ")} กรุณาอัปโหลดให้ครบก่อนส่ง FDH จริง`);
      } else {
        clearAlert();
      }
    }

    function renderAutofillTable(result) {
      const rows = result.autofillRows || [];
      if (!rows.length) {
        el.autofillTable.innerHTML = `<tr><td colspan="9" class="muted">ยังไม่มีเคสที่ต้องเติม ODX K020 หรือ ADP 4|15001</td></tr>`;
        return;
      }

      const preview = rows.slice(0, 200);
      el.autofillTable.innerHTML = preview.map((row, index) => {
        const v = row.values || {};
        const status = state.autofillEnabled ? "สำเร็จ" : "รอเติม";
        const statusClass = state.autofillEnabled ? "ok" : "muted";
        return `<tr>
          <td>${(index + 1).toLocaleString("th-TH")}</td>
          <td class="${statusClass}">${status}</td>
          <td>${escapeHtml(v.HN)}</td>
          <td>${escapeHtml(v.FILE)}</td>
          <td>${escapeHtml(v.SEQ)}</td>
          <td>${escapeHtml(v.DATE || v.DATEDX)}</td>
          <td>${escapeHtml(v.CODE || v.DIAG)}</td>
          <td>${escapeHtml(v.DETAIL || `DXTYPE=${v.DXTYPE || ""} CLINIC=${v.CLINIC || ""}`)}</td>
          <td>${escapeHtml(v.TOTAL || "")}</td>
          <td>${escapeHtml(v.FIELDS)}</td>
        </tr>`;
      }).join("");

      if (rows.length > preview.length) {
        el.autofillTable.insertAdjacentHTML("beforeend", `<tr><td colspan="9" class="muted">แสดง 200 รายการแรกจากทั้งหมด ${rows.length.toLocaleString("th-TH")} รายการ</td></tr>`);
      }
    }

    function renderReferCleanupTable(result) {
      const rows = result.referCleanup?.previewRows || [];
      if (!rows.length) {
        el.referCleanupTable.innerHTML = `<tr><td colspan="9" class="muted">ยังไม่มีรายการ refer ที่ต้องลบ</td></tr>`;
        return;
      }

      if (!state.referCleanupEnabled) {
        el.referCleanupTable.innerHTML = `<tr><td colspan="9" class="muted">พบ ${rows.length.toLocaleString("th-TH")} รายการ refer กดปุ่ม "ลบ refer" เพื่อแสดงผลและนำไปใช้ตอน export</td></tr>`;
        return;
      }

      const preview = rows.slice(0, 200);
      el.referCleanupTable.innerHTML = preview.map((v, index) => {
        return `<tr>
          <td>${(index + 1).toLocaleString("th-TH")}</td>
          <td class="ok">สำเร็จ</td>
          <td>${escapeHtml(v.HN)}</td>
          <td>${escapeHtml(v.FILE)}</td>
          <td>${escapeHtml(v.DATE)}</td>
          <td>${escapeHtml(v.CODE)}</td>
          <td>${escapeHtml(v.DETAIL)}</td>
          <td>${escapeHtml(v.SEQ)}</td>
          <td>${escapeHtml(v.TOTAL)}</td>
        </tr>`;
      }).join("");

      if (rows.length > preview.length) {
        el.referCleanupTable.insertAdjacentHTML("beforeend", `<tr><td colspan="9" class="muted">แสดง 200 รายการแรกจากทั้งหมด ${rows.length.toLocaleString("th-TH")} รายการ</td></tr>`);
      }
    }

    function formatMoney(cents) {
      return (cents / 100).toLocaleString("th-TH", {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2
      });
    }

    function escapeHtml(value) {
      return String(value || "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39;");
    }

    function setBusy(busy, message) {
      el.statusPill.textContent = message;
      el.exportButton.disabled = busy || !(state.result && state.result.cases.length);
      el.autofillButton.disabled = busy || !(state.result && state.result.autofillRows && state.result.autofillRows.length) || state.autofillEnabled;
      el.referCleanupButton.disabled = busy || !(state.result && state.result.referCleanup && state.result.referCleanup.previewRows.length) || state.referCleanupEnabled;
    }

    function showAlert(message) {
      el.alertBox.textContent = message;
      el.alertBox.classList.add("show");
    }

    function clearAlert() {
      el.alertBox.textContent = "";
      el.alertBox.classList.remove("show");
    }

    function clearAll() {
      state.tables = new Map();
      state.result = null;
      state.autofillEnabled = false;
      state.referCleanupEnabled = false;
      el.fileInput.value = "";
      el.folderInput.value = "";
      el.uploadStatus.textContent = "ยังไม่ได้อัปโหลดไฟล์";
      el.statusPill.textContent = "พร้อมใช้งาน";
      clearAlert();
      render();
    }

    function buildOutputBytes(table) {
      const parts = [table.headerLine];
      const cleanup = state.referCleanupEnabled ? state.result?.referCleanup : null;
      const adjustedChtKeys = new Set();
      if (!(table.name === "ORF" && state.referCleanupEnabled)) {
        for (const row of table.filteredRows) {
          if (state.referCleanupEnabled && table.name === "ADP" && isReferAdpRow(row.values)) continue;
          if (state.referCleanupEnabled && table.name === "CHA" && isReferChaRow(row.values)) continue;
          if (table.name === "CHT" && cleanup) {
            const key = visitKey(row.values);
            const deduction = adjustedChtKeys.has(key) ? 0 : (cleanup.chaDeductions.get(key) || 0);
            if (deduction) {
              adjustedChtKeys.add(key);
              parts.push(rowLineWithOverrides(table, row, { TOTAL: centsToField(toCents(row.values.TOTAL) - deduction) }));
              continue;
            }
          }
          parts.push(row.line);
        }
      }
      if (table.name === "ODX" && state.autofillEnabled && state.result) {
        for (const row of state.result.autofillOdxRows) parts.push(row.line);
      }
      if (table.name === "ADP" && state.autofillEnabled && state.result) {
        for (const row of state.result.autofillAdpRows) parts.push(row.line);
      }
      if ((table.name === "OOP" || table.name === "IOP") && state.autofillEnabled && state.result) {
        for (const row of state.result.autofillIcd9Rows.filter(item => item.values.FILE === table.name)) parts.push(row.line);
      }
      return joinBytes(parts, table.newline);
    }

    function joinBytes(lines, newline) {
      const total = lines.reduce((sum, line) => sum + line.length, 0) + (newline.length * lines.length);
      const out = new Uint8Array(total);
      let offset = 0;
      for (const line of lines) {
        out.set(line, offset);
        offset += line.length;
        out.set(newline, offset);
        offset += newline.length;
      }
      return out;
    }

    async function exportZip() {
      if (!state.result || !state.result.cases.length) return;
      const files = [];
      for (const name of EXPECTED_FILES) {
        const table = state.tables.get(name);
        if (!table) continue;
        files.push({ name: `${name}.txt`, data: buildOutputBytes(table) });
      }
      const zip = makeZip(files);
      const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
      downloadBlob(zip, `FDH_K020_ADP15001_${stamp}.zip`, "application/zip");
    }

    function applyAutofill() {
      if (!state.result || !state.result.autofillRows.length || state.autofillEnabled) return;
      state.autofillEnabled = true;
      analyze();
    }

    function applyReferCleanup() {
      if (!state.result || !state.result.referCleanup.previewRows.length || state.referCleanupEnabled) return;
      state.referCleanupEnabled = true;
      analyze();
    }

    function downloadBlob(bytes, filename, type) {
      const blob = new Blob([bytes], { type });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    function readU16(bytes, offset) {
      return bytes[offset] | (bytes[offset + 1] << 8);
    }

    function readU32(bytes, offset) {
      return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
    }

    function findEndOfCentralDirectory(bytes) {
      const min = Math.max(0, bytes.length - 0xffff - 22);
      for (let i = bytes.length - 22; i >= min; i -= 1) {
        if (readU32(bytes, i) === 0x06054b50) return i;
      }
      return -1;
    }

    async function inflateRaw(bytes) {
      if (typeof DecompressionStream === "undefined") {
        throw new Error("browser นี้ยังไม่รองรับการแตก zip แบบ deflate กรุณาเปิดด้วย Chrome หรือ Edge รุ่นใหม่");
      }
      try {
        const rawStream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
        return new Uint8Array(await new Response(rawStream).arrayBuffer());
      } catch (rawError) {
        const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate"));
        return new Uint8Array(await new Response(stream).arrayBuffer());
      }
    }

    async function readZipTextEntries(file) {
      const bytes = new Uint8Array(await file.arrayBuffer());
      const eocd = findEndOfCentralDirectory(bytes);
      if (eocd < 0) throw new Error("ไม่พบโครงสร้าง zip ที่ถูกต้อง");

      const entryCount = readU16(bytes, eocd + 10);
      const centralOffset = readU32(bytes, eocd + 16);
      const decoder = new TextDecoder("utf-8");
      const entries = [];
      let offset = centralOffset;

      for (let i = 0; i < entryCount; i += 1) {
        if (readU32(bytes, offset) !== 0x02014b50) throw new Error("central directory เสียหาย");
        const method = readU16(bytes, offset + 10);
        const compressedSize = readU32(bytes, offset + 20);
        const fileNameLength = readU16(bytes, offset + 28);
        const extraLength = readU16(bytes, offset + 30);
        const commentLength = readU16(bytes, offset + 32);
        const localOffset = readU32(bytes, offset + 42);
        const nameBytes = bytes.slice(offset + 46, offset + 46 + fileNameLength);
        const entryName = decoder.decode(nameBytes).replace(/\\/g, "/");
        offset += 46 + fileNameLength + extraLength + commentLength;

        if (entryName.endsWith("/") || !/\.txt$/i.test(entryName)) continue;
        if (readU32(bytes, localOffset) !== 0x04034b50) throw new Error(`${entryName}: local header เสียหาย`);

        const localNameLength = readU16(bytes, localOffset + 26);
        const localExtraLength = readU16(bytes, localOffset + 28);
        const dataStart = localOffset + 30 + localNameLength + localExtraLength;
        const compressed = bytes.slice(dataStart, dataStart + compressedSize);
        let data;
        if (method === 0) {
          data = compressed;
        } else if (method === 8) {
          data = await inflateRaw(compressed);
        } else {
          throw new Error(`${entryName}: ยังไม่รองรับ compression method ${method}`);
        }

        entries.push({
          name: entryName.split("/").pop(),
          entryName,
          baseName: entryName.split("/").pop().replace(/\.[^.]+$/, "").toUpperCase(),
          bytes: data,
          async arrayBuffer() {
            return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
          }
        });
      }

      return entries;
    }

    const crcTable = (() => {
      const table = new Uint32Array(256);
      for (let i = 0; i < 256; i += 1) {
        let c = i;
        for (let k = 0; k < 8; k += 1) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
        table[i] = c >>> 0;
      }
      return table;
    })();

    function crc32(bytes) {
      let crc = 0xffffffff;
      for (const byte of bytes) crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
      return (crc ^ 0xffffffff) >>> 0;
    }

    function dosDateTime(date) {
      const year = Math.max(1980, date.getFullYear());
      const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
      const day = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
      return { time, day };
    }

    function writeU16(view, offset, value) {
      view.setUint16(offset, value, true);
    }

    function writeU32(view, offset, value) {
      view.setUint32(offset, value >>> 0, true);
    }

    function makeZip(files) {
      const encoder = new TextEncoder();
      const now = dosDateTime(new Date());
      const locals = [];
      const centrals = [];
      let offset = 0;

      for (const file of files) {
        const nameBytes = encoder.encode(file.name);
        const data = file.data;
        const crc = crc32(data);
        const local = new Uint8Array(30 + nameBytes.length + data.length);
        const localView = new DataView(local.buffer);
        writeU32(localView, 0, 0x04034b50);
        writeU16(localView, 4, 20);
        writeU16(localView, 6, 0);
        writeU16(localView, 8, 0);
        writeU16(localView, 10, now.time);
        writeU16(localView, 12, now.day);
        writeU32(localView, 14, crc);
        writeU32(localView, 18, data.length);
        writeU32(localView, 22, data.length);
        writeU16(localView, 26, nameBytes.length);
        writeU16(localView, 28, 0);
        local.set(nameBytes, 30);
        local.set(data, 30 + nameBytes.length);
        locals.push(local);

        const central = new Uint8Array(46 + nameBytes.length);
        const centralView = new DataView(central.buffer);
        writeU32(centralView, 0, 0x02014b50);
        writeU16(centralView, 4, 20);
        writeU16(centralView, 6, 20);
        writeU16(centralView, 8, 0);
        writeU16(centralView, 10, 0);
        writeU16(centralView, 12, now.time);
        writeU16(centralView, 14, now.day);
        writeU32(centralView, 16, crc);
        writeU32(centralView, 20, data.length);
        writeU32(centralView, 24, data.length);
        writeU16(centralView, 28, nameBytes.length);
        writeU16(centralView, 30, 0);
        writeU16(centralView, 32, 0);
        writeU16(centralView, 34, 0);
        writeU16(centralView, 36, 0);
        writeU32(centralView, 38, 0);
        writeU32(centralView, 42, offset);
        central.set(nameBytes, 46);
        centrals.push(central);

        offset += local.length;
      }

      const centralOffset = offset;
      const centralSize = centrals.reduce((sum, item) => sum + item.length, 0);
      const end = new Uint8Array(22);
      const endView = new DataView(end.buffer);
      writeU32(endView, 0, 0x06054b50);
      writeU16(endView, 4, 0);
      writeU16(endView, 6, 0);
      writeU16(endView, 8, files.length);
      writeU16(endView, 10, files.length);
      writeU32(endView, 12, centralSize);
      writeU32(endView, 16, centralOffset);
      writeU16(endView, 20, 0);

      return concatBytes([...locals, ...centrals, end]);
    }

    function concatBytes(parts) {
      const total = parts.reduce((sum, part) => sum + part.length, 0);
      const out = new Uint8Array(total);
      let offset = 0;
      for (const part of parts) {
        out.set(part, offset);
        offset += part.length;
      }
      return out;
    }

    el.fileInput.addEventListener("change", event => loadFiles(event.target.files));
    el.folderInput.addEventListener("change", event => loadFiles(event.target.files));
    el.clearButton.addEventListener("click", clearAll);
    el.autofillButton.addEventListener("click", applyAutofill);
    el.referCleanupButton.addEventListener("click", applyReferCleanup);
    el.exportButton.addEventListener("click", exportZip);

    for (const name of ["dragenter", "dragover"]) {
      el.dropzone.addEventListener(name, event => {
        event.preventDefault();
        el.dropzone.classList.add("drag");
      });
    }
    for (const name of ["dragleave", "drop"]) {
      el.dropzone.addEventListener(name, event => {
        event.preventDefault();
        el.dropzone.classList.remove("drag");
      });
    }
    el.dropzone.addEventListener("drop", event => loadFiles(event.dataTransfer.files));

    render();
  </script>
</body>
</html>

Youez - 2016 - github.com/yon3zu
LinuXploit