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/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/paphayomho/domains/paphayomhospital.go.th/private_html/hdc-precheck.html
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HDC Pre-Check 43 Files | โรงพยาบาลป่าพะยอม</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');

  :root {
    --primary: #0d6e4d;
    --primary-light: #16a372;
    --primary-dark: #094d37;
    --accent: #00b4d8;
    --accent2: #f59e0b;
    --danger: #dc2626;
    --warning: #f59e0b;
    --info: #0ea5e9;
    --success: #16a34a;
    --bg: #f0f7f4;
    --bg2: #ffffff;
    --bg3: #e8f4ef;
    --border: #d1e7dd;
    --text: #1a2e25;
    --text2: #4a6b5a;
    --text3: #7a9b8a;
    --shadow: 0 2px 8px rgba(13,110,77,0.12);
    --shadow-lg: 0 8px 32px rgba(13,110,77,0.15);
    --radius: 10px;
    --radius-sm: 6px;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: 'Sarabun', sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; }
  
  /* ===== TOPBAR ===== */
  .topbar {
    background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 60%, var(--primary-light) 100%);
    color: white; padding: 0 24px;
    display: flex; align-items: center; gap: 16px;
    height: 58px; box-shadow: 0 2px 12px rgba(0,0,0,0.2); position: sticky; top:0; z-index:100;
  }
  .topbar-logo { font-size: 20px; font-weight: 700; letter-spacing: -0.5px; }
  .topbar-logo span { color: #7ee8c8; }
  .topbar-sub { font-size: 12px; color: rgba(255,255,255,0.7); margin-left: 4px; }
  .topbar-spacer { flex: 1; }
  .topbar-hcode { background: rgba(255,255,255,0.15); border-radius: 20px; padding: 4px 14px; font-size: 12px; font-weight: 600; font-family: 'IBM Plex Mono', monospace; }

  /* ===== NAV TABS ===== */
  .navtabs { background: var(--bg2); border-bottom: 2px solid var(--border); display: flex; overflow-x: auto; }
  .navtab { padding: 12px 20px; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text2); white-space: nowrap; border-bottom: 3px solid transparent; transition: all .2s; display: flex; align-items: center; gap: 6px; }
  .navtab:hover { color: var(--primary); background: var(--bg3); }
  .navtab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 700; }
  .navtab .badge { background: var(--danger); color: white; border-radius: 10px; padding: 1px 6px; font-size: 10px; font-weight: 700; }
  .navtab .badge.ok { background: var(--success); }
  .navtab .badge.warn { background: var(--warning); }

  /* ===== LAYOUT ===== */
  .container { max-width: 1400px; margin: 0 auto; padding: 20px 24px; }
  .page { display: none; }
  .page.active { display: block; }

  /* ===== CARDS ===== */
  .card { background: var(--bg2); border-radius: var(--radius); box-shadow: var(--shadow); padding: 20px; margin-bottom: 16px; border: 1px solid var(--border); }
  .card-title { font-size: 16px; font-weight: 700; color: var(--primary-dark); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
  .card-title .icon { width: 28px; height: 28px; border-radius: 6px; background: var(--primary); color: white; display: flex; align-items: center; justify-content: center; font-size: 14px; }

  /* ===== UPLOAD ZONE ===== */
  #dropzone {
    border: 3px dashed var(--primary-light); border-radius: var(--radius); padding: 60px 40px;
    text-align: center; cursor: pointer; transition: all .3s; background: var(--bg3); position: relative;
  }
  #dropzone:hover, #dropzone.drag { border-color: var(--primary); background: #d4ede5; }
  #dropzone .dz-icon { font-size: 52px; margin-bottom: 12px; }
  #dropzone .dz-title { font-size: 20px; font-weight: 700; color: var(--primary-dark); margin-bottom: 6px; }
  #dropzone .dz-sub { color: var(--text2); font-size: 14px; }
  #fileInput { display: none; }

  /* ===== PROGRESS ===== */
  .progress-wrap { margin: 20px 0; }
  .progress-label { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 13px; font-weight: 600; color: var(--text2); }
  .progress-bar { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; }
  .progress-fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--accent)); border-radius: 4px; transition: width .3s ease; }
  .status-text { text-align: center; color: var(--text2); font-size: 13px; margin-top: 8px; font-style: italic; }

  /* ===== KPI GRID ===== */
  .kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 20px; }
  .kpi-card { background: var(--bg2); border-radius: var(--radius); padding: 16px; border: 1px solid var(--border); text-align: center; }
  .kpi-card.green { border-top: 4px solid var(--success); }
  .kpi-card.red { border-top: 4px solid var(--danger); }
  .kpi-card.yellow { border-top: 4px solid var(--warning); }
  .kpi-card.blue { border-top: 4px solid var(--accent); }
  .kpi-card.purple { border-top: 4px solid #8b5cf6; }
  .kpi-val { font-size: 28px; font-weight: 800; font-family: 'IBM Plex Mono', monospace; color: var(--primary-dark); line-height: 1; }
  .kpi-val.red { color: var(--danger); }
  .kpi-val.yellow { color: #b45309; }
  .kpi-label { font-size: 12px; color: var(--text2); margin-top: 4px; line-height: 1.3; }

  /* ===== SCORE RING ===== */
  .score-ring-wrap { display: flex; align-items: center; justify-content: center; gap: 32px; padding: 20px; flex-wrap: wrap; }
  .score-ring { position: relative; width: 140px; height: 140px; }
  .score-ring canvas { width: 140px !important; height: 140px !important; }
  .score-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
  .score-num { font-size: 32px; font-weight: 900; color: var(--primary-dark); font-family: 'IBM Plex Mono', monospace; }
  .score-label { font-size: 11px; color: var(--text2); }

  /* ===== TABLES ===== */
  .tbl-wrap { overflow-x: auto; border-radius: var(--radius-sm); border: 1px solid var(--border); }
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  thead th { background: var(--primary); color: white; padding: 10px 12px; text-align: left; font-weight: 600; white-space: nowrap; position: sticky; top: 0; z-index: 2; }
  tbody tr { border-bottom: 1px solid var(--border); transition: background .15s; }
  tbody tr:hover { background: var(--bg3); }
  tbody td { padding: 8px 12px; vertical-align: middle; }
  tbody tr:nth-child(even) { background: #f9fdfb; }
  tbody tr:nth-child(even):hover { background: var(--bg3); }

  /* ===== SEVERITY BADGES ===== */
  .sev { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 700; white-space: nowrap; }
  .sev-critical { background: #fee2e2; color: #991b1b; }
  .sev-high { background: #ffedd5; color: #92400e; }
  .sev-medium { background: #fef9c3; color: #713f12; }
  .sev-low { background: #dbeafe; color: #1e40af; }
  .sev-info { background: #f0fdf4; color: #166534; }

  /* ===== BUTTONS ===== */
  .btn { display: inline-flex; align-items: center; gap: 6px; padding: 9px 18px; border-radius: var(--radius-sm); font-family: 'Sarabun', sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; border: none; transition: all .2s; white-space: nowrap; }
  .btn-primary { background: var(--primary); color: white; }
  .btn-primary:hover { background: var(--primary-dark); }
  .btn-success { background: var(--success); color: white; }
  .btn-success:hover { background: #15803d; }
  .btn-warning { background: var(--warning); color: white; }
  .btn-warning:hover { background: #d97706; }
  .btn-danger { background: var(--danger); color: white; }
  .btn-info { background: var(--accent); color: white; }
  .btn-outline { background: transparent; color: var(--primary); border: 2px solid var(--primary); }
  .btn-outline:hover { background: var(--bg3); }
  .btn-sm { padding: 5px 12px; font-size: 12px; }
  .btn:disabled { opacity: .5; cursor: not-allowed; }
  .btn-group { display: flex; gap: 8px; flex-wrap: wrap; }

  /* ===== FILE STATUS TABLE ===== */
  .file-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
  .file-item { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 14px; display: flex; align-items: center; gap: 10px; }
  .file-item .fi-icon { font-size: 20px; }
  .file-item .fi-name { font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 600; color: var(--primary-dark); }
  .file-item .fi-info { font-size: 11px; color: var(--text2); }
  .file-item .fi-status { margin-left: auto; }
  .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
  .dot-ok { background: var(--success); }
  .dot-err { background: var(--danger); }
  .dot-warn { background: var(--warning); }

  /* ===== ALERT BOXES ===== */
  .alert { padding: 12px 16px; border-radius: var(--radius-sm); margin-bottom: 12px; border-left: 4px solid; display: flex; gap: 10px; align-items: flex-start; }
  .alert-info { background: #eff6ff; border-color: var(--info); color: #1e40af; }
  .alert-warning { background: #fffbeb; border-color: var(--warning); color: #92400e; }
  .alert-danger { background: #fef2f2; border-color: var(--danger); color: #991b1b; }
  .alert-success { background: #f0fdf4; border-color: var(--success); color: #166534; }

  /* ===== FILTER BAR ===== */
  .filter-bar { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; align-items: center; }
  .filter-bar select, .filter-bar input { padding: 7px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-family: 'Sarabun', sans-serif; font-size: 13px; background: white; color: var(--text); outline: none; }
  .filter-bar select:focus, .filter-bar input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(13,110,77,0.1); }
  .filter-label { font-weight: 600; font-size: 13px; color: var(--text2); }
  .count-badge { background: var(--primary); color: white; border-radius: 12px; padding: 2px 10px; font-size: 12px; font-weight: 700; margin-left: auto; }

  /* ===== CHARTS GRID ===== */
  .charts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  @media(max-width:900px) { .charts-grid { grid-template-columns: 1fr; } }
  .chart-box { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
  .chart-box canvas { max-height: 220px; }

  /* ===== AUDIT LOG ===== */
  .log-entry { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); align-items: flex-start; font-size: 12px; }
  .log-time { font-family: 'IBM Plex Mono', monospace; color: var(--text3); white-space: nowrap; font-size: 11px; }
  .log-action { font-weight: 600; }
  .log-detail { color: var(--text2); }

  /* ===== MONO ===== */
  .mono { font-family: 'IBM Plex Mono', monospace; font-size: 12px; }

  /* ===== LOADING OVERLAY ===== */
  #loadingOverlay { position: fixed; inset: 0; background: rgba(9,77,55,0.85); display: none; align-items: center; justify-content: center; z-index: 9999; flex-direction: column; gap: 20px; }
  #loadingOverlay.show { display: flex; }
  .spinner { width: 48px; height: 48px; border: 5px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 0.8s linear infinite; }
  @keyframes spin { to { transform: rotate(360deg); } }
  .loading-text { color: white; font-size: 16px; font-weight: 600; }
  .loading-sub { color: rgba(255,255,255,0.7); font-size: 13px; }

  /* ===== MODAL ===== */
  .modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 500; display: none; align-items: center; justify-content: center; padding: 20px; }
  .modal-backdrop.show { display: flex; }
  .modal { background: white; border-radius: var(--radius); max-width: 720px; width: 100%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); }
  .modal-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
  .modal-title { font-size: 16px; font-weight: 700; color: var(--primary-dark); }
  .modal-body { padding: 20px; overflow-y: auto; flex: 1; }
  .modal-footer { padding: 12px 20px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }
  .modal-close { background: none; border: none; font-size: 20px; cursor: pointer; color: var(--text2); }

  /* ===== MAPPING TABLE ===== */
  .map-row { display: grid; grid-template-columns: 120px 1fr 120px 120px 80px; gap: 8px; align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border); font-size: 12px; }
  .map-header { font-weight: 700; color: var(--primary-dark); background: var(--bg3); padding: 8px 0; }

  /* ===== SECTION DIVIDER ===== */
  .section-div { display: flex; align-items: center; gap: 12px; margin: 24px 0 16px; }
  .section-div span { font-size: 15px; font-weight: 700; color: var(--primary-dark); white-space: nowrap; }
  .section-div::before, .section-div::after { content:''; flex:1; height:2px; background: var(--border); }

  /* ===== EMPTY STATE ===== */
  .empty-state { text-align: center; padding: 60px 20px; color: var(--text2); }
  .empty-state .es-icon { font-size: 56px; margin-bottom: 12px; }
  .empty-state .es-title { font-size: 18px; font-weight: 700; color: var(--text); margin-bottom: 6px; }

  /* ===== TYPEAREA BAR ===== */
  .typearea-bars { display: flex; gap: 8px; margin-top: 12px; }
  .ta-bar { flex: 1; text-align: center; }
  .ta-bar-inner { border-radius: 4px 4px 0 0; min-height: 8px; }
  .ta-bar-label { font-size: 11px; font-weight: 700; margin-top: 4px; }
  .ta-bar-val { font-size: 13px; font-weight: 800; font-family: 'IBM Plex Mono', monospace; }

  /* ===== FIX PREVIEW ===== */
  .fix-preview { background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; font-size: 12px; }
  .fix-row { display: flex; gap: 8px; align-items: center; padding: 4px 0; }
  .fix-from { text-decoration: line-through; color: var(--danger); font-family: 'IBM Plex Mono', monospace; }
  .fix-to { color: var(--success); font-weight: 700; font-family: 'IBM Plex Mono', monospace; }

  /* ===== MANUAL EDIT ===== */
  .edit-grid { display: grid; grid-template-columns: 150px 1fr; gap: 10px 12px; align-items: center; }
  .edit-grid label { font-size: 12px; font-weight: 700; color: var(--text2); }
  .edit-grid input, .edit-grid select, .edit-grid textarea {
    width: 100%; border: 1px solid var(--border); border-radius: var(--radius-sm);
    padding: 8px 10px; font-family: 'Sarabun', sans-serif; font-size: 13px; color: var(--text);
  }
  .edit-grid input:focus, .edit-grid select:focus, .edit-grid textarea:focus {
    outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(13,110,77,0.1);
  }
  .edit-help { margin-top: 12px; color: var(--text2); font-size: 12px; line-height: 1.5; }
  .action-stack { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }

  /* ===== RESPONSIVE ===== */
  @media(max-width:600px) {
    .container { padding: 12px; }
    .kpi-grid { grid-template-columns: repeat(2,1fr); }
    .topbar { padding: 0 12px; }
    .charts-grid { grid-template-columns: 1fr; }
  }
</style>
</head>
<body>

<!-- Loading Overlay -->
<div id="loadingOverlay">
  <div class="spinner"></div>
  <div class="loading-text" id="loadingText">กำลังประมวลผล...</div>
  <div class="loading-sub" id="loadingSub">กรุณารอสักครู่</div>
</div>

<!-- Modal -->
<div class="modal-backdrop" id="modalBackdrop">
  <div class="modal">
    <div class="modal-header">
      <div class="modal-title" id="modalTitle">รายละเอียด</div>
      <button class="modal-close" onclick="closeModal()">✕</button>
    </div>
    <div class="modal-body" id="modalBody"></div>
    <div class="modal-footer" id="modalFooter">
      <button class="btn btn-outline btn-sm" onclick="closeModal()">ปิด</button>
    </div>
  </div>
</div>

<!-- Top Bar -->
<div class="topbar">
  <div class="topbar-logo">HDC Pre<span>-</span>Check</div>
  <div class="topbar-sub">ตรวจสอบ 43 แฟ้มก่อน Upload</div>
  <div class="topbar-spacer"></div>
  <div class="topbar-hcode" id="topHcode">HCODE: —</div>
</div>

<!-- Nav Tabs -->
<div class="navtabs" id="navtabs">
  <div class="navtab active" onclick="showPage('upload')" id="tab-upload">📁 Upload & Summary</div>
  <div class="navtab" onclick="showPage('dashboard')" id="tab-dashboard">📊 Dashboard</div>
  <div class="navtab" onclick="showPage('errors')" id="tab-errors">⚠️ Error Table <span class="badge" id="errBadge">—</span></div>
  <div class="navtab" onclick="showPage('person')" id="tab-person">👤 PERSON</div>
  <div class="navtab" onclick="showPage('chronic')" id="tab-chronic">🏥 CHRONIC/FU</div>
  <div class="navtab" onclick="showPage('lab')" id="tab-lab">🧪 LABFU</div>
  <div class="navtab" onclick="showPage('mapping')" id="tab-mapping">🔗 Lab Mapping</div>
  <div class="navtab" onclick="showPage('audit')" id="tab-audit">📋 Audit Log</div>
  <div class="navtab" onclick="showPage('export')" id="tab-export">💾 Export</div>
</div>

<!-- ===== PAGES ===== -->
<div class="container">

<!-- PAGE: UPLOAD -->
<div class="page active" id="page-upload">
  <div class="card">
    <div class="card-title"><div class="icon">📁</div> อัปโหลดไฟล์ 43 แฟ้ม</div>
    <div id="dropzone" onclick="document.getElementById('fileInput').click()">
      <div class="dz-icon">🗜️</div>
      <div class="dz-title">วาง ZIP ไฟล์ 43 แฟ้มที่นี่</div>
      <div class="dz-sub">หรือคลิกเพื่อเลือกไฟล์ • รองรับ UTF-8 และ TIS-620/CP874</div>
      <div class="dz-sub" style="margin-top:8px;font-size:12px;color:var(--text3)">ไฟล์ทั้งหมดประมวลผลใน Browser ไม่ส่งข้อมูลออกภายนอก</div>
    </div>
    <input type="file" id="fileInput" accept=".zip" onchange="handleFileSelect(event)">
    <div class="progress-wrap" id="progressWrap" style="display:none">
      <div class="progress-label"><span id="progressLabel">อ่านไฟล์...</span><span id="progressPct">0%</span></div>
      <div class="progress-bar"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
      <div class="status-text" id="statusText"></div>
    </div>
  </div>

  <div id="fileSummarySection" style="display:none">
    <div class="card">
      <div class="card-title"><div class="icon">📄</div> สรุปไฟล์ในชุดข้อมูล</div>
      <div class="kpi-grid" id="fileSummaryKpi"></div>
      <div class="file-grid" id="fileGrid"></div>
    </div>
    <div class="btn-group" style="margin-top:8px">
      <button class="btn btn-primary" onclick="runAllChecks()">🔍 ตรวจสอบทั้งหมด</button>
      <button class="btn btn-outline" onclick="resetApp()">🔄 โหลดไฟล์ใหม่</button>
    </div>
  </div>
</div>

<!-- PAGE: DASHBOARD -->
<div class="page" id="page-dashboard">
  <div id="dashEmpty" class="empty-state">
    <div class="es-icon">📊</div>
    <div class="es-title">ยังไม่มีข้อมูล</div>
    <div>กรุณาอัปโหลดและตรวจสอบไฟล์ก่อน</div>
  </div>
  <div id="dashContent" style="display:none">
    <div class="score-ring-wrap card">
      <div>
        <div class="score-ring"><canvas id="scoreChart"></canvas>
        <div class="score-center"><div class="score-num" id="scoreNum">—</div><div class="score-label">คะแนน HDC</div></div></div>
      </div>
      <div style="flex:1;min-width:200px">
        <div class="kpi-grid" id="dashKpi"></div>
      </div>
    </div>
    <div class="charts-grid">
      <div class="chart-box">
        <div style="font-weight:700;color:var(--primary-dark);margin-bottom:8px">Error แยกตาม Severity</div>
        <canvas id="sevChart"></canvas>
      </div>
      <div class="chart-box">
        <div style="font-weight:700;color:var(--primary-dark);margin-bottom:8px">Error แยกตาม Module</div>
        <canvas id="moduleChart"></canvas>
      </div>
      <div class="chart-box">
        <div style="font-weight:700;color:var(--primary-dark);margin-bottom:8px">TYPEAREA Distribution</div>
        <canvas id="typeareaChart"></canvas>
      </div>
      <div class="chart-box">
        <div style="font-weight:700;color:var(--primary-dark);margin-bottom:8px">Top 10 Error ที่พบบ่อย</div>
        <canvas id="top10Chart"></canvas>
      </div>
    </div>
    <div class="card" id="dashAlerts"></div>
  </div>
</div>

<!-- PAGE: ERRORS -->
<div class="page" id="page-errors">
  <div class="card">
    <div class="card-title"><div class="icon">⚠️</div> ตารางแสดง Error ทั้งหมด</div>
    <div class="filter-bar">
      <span class="filter-label">กรอง:</span>
      <select id="filterSev" onchange="renderErrorTable()">
        <option value="">ทุก Severity</option>
        <option value="critical">Critical</option>
        <option value="high">High</option>
        <option value="medium">Medium</option>
        <option value="low">Low</option>
      </select>
      <select id="filterFile" onchange="renderErrorTable()">
        <option value="">ทุกไฟล์</option>
      </select>
      <select id="filterModule" onchange="renderErrorTable()">
        <option value="">ทุก Module</option>
        <option value="PERSON">PERSON</option>
        <option value="SERVICE">SERVICE</option>
        <option value="CHRONIC">CHRONIC</option>
        <option value="CHRONICFU">CHRONICFU</option>
        <option value="LABFU">LABFU</option>
        <option value="DIAGNOSIS_OPD">DIAGNOSIS_OPD</option>
        <option value="PROCEDURE_OPD">PROCEDURE_OPD</option>
      </select>
      <input type="text" id="filterSearch" placeholder="ค้นหา PID / CID / SEQ..." onkeyup="renderErrorTable()">
      <span class="count-badge" id="errCount">0 รายการ</span>
    </div>
    <div class="tbl-wrap" style="max-height:520px;overflow-y:auto">
      <table id="errTable">
        <thead>
          <tr>
            <th>#</th>
            <th>Severity</th>
            <th>Module</th>
            <th>Row</th>
            <th>PID</th>
            <th>CID</th>
            <th>SEQ</th>
            <th>Field</th>
            <th>ค่าปัจจุบัน</th>
            <th>ค่าที่แนะนำ</th>
            <th>คำอธิบาย</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody id="errTableBody"></tbody>
      </table>
    </div>
    <div class="btn-group" style="margin-top:12px">
      <button class="btn btn-warning" onclick="autoFixSafe()">⚡ Auto-fix ที่ปลอดภัย</button>
      <button class="btn btn-outline" onclick="previewFixes()">👁️ Preview ก่อน Fix</button>
    </div>
  </div>
</div>

<!-- PAGE: PERSON -->
<div class="page" id="page-person">
  <div class="card">
    <div class="card-title"><div class="icon">👤</div> PERSON / TYPEAREA Audit</div>
    <div id="personStats"></div>
  </div>
  <div class="card">
    <div class="card-title"><div class="icon">📍</div> TYPEAREA Distribution</div>
    <div id="typeareaDetail"></div>
  </div>
  <div class="card">
    <div class="card-title"><div class="icon">✏️</div> แก้ TYPEAREA ใน PERSON</div>
    <div class="filter-bar">
      <span class="filter-label">PID รายคน:</span>
      <input type="text" id="taPid" placeholder="เว้นว่าง = แก้ทั้งชุด">
      <span class="filter-label">จาก</span>
      <select id="taFrom">
        <option value="">ทุกค่า</option>
        <option value="1">1 ทะเบียนในเขตและอยู่จริง</option>
        <option value="2">2 ทะเบียนในเขตแต่ตัวไม่อยู่</option>
        <option value="3">3 อยู่จริงในเขต ทะเบียนนอกเขต</option>
        <option value="4" selected>4 นอกเขตแต่มารับบริการ</option>
        <option value="5">5 อื่นๆ</option>
      </select>
      <span class="filter-label">เป็น</span>
      <select id="taTo">
        <option value="1" selected>1 ทะเบียนในเขตและอยู่จริง</option>
        <option value="2">2 ทะเบียนในเขตแต่ตัวไม่อยู่</option>
        <option value="3">3 อยู่จริงในเขต ทะเบียนนอกเขต</option>
        <option value="4">4 นอกเขตแต่มารับบริการ</option>
        <option value="5">5 อื่นๆ</option>
      </select>
      <button class="btn btn-primary btn-sm" onclick="applyTypeareaEdit()">บันทึก TYPEAREA</button>
    </div>
    <div class="edit-help">ถ้าใส่ PID จะเปลี่ยนเฉพาะรายนั้น ถ้าเว้นว่างจะเปลี่ยนทุกคนที่ TYPEAREA ตรงกับช่อง “จาก” เช่น 4 → 1 หรือ 4 → 3 ทั้งชุด</div>
  </div>
  <div class="card" id="personIssuesCard" style="display:none">
    <div class="card-title"><div class="icon">🚨</div> รายการที่ต้องตรวจสอบ</div>
    <div id="personIssues"></div>
  </div>
</div>

<!-- PAGE: CHRONIC -->
<div class="page" id="page-chronic">
  <div class="card">
    <div class="card-title"><div class="icon">🏥</div> CHRONIC Registry Audit</div>
    <div id="chronicStats"></div>
  </div>
  <div class="card">
    <div class="card-title"><div class="icon">📋</div> CHRONICFU Coverage</div>
    <div id="chronicFuStats"></div>
  </div>
  <div class="card" id="chronicMissingFuCard">
    <div class="card-title"><div class="icon">❗</div> CHRONIC ที่ไม่มี FU Visit</div>
    <div id="chronicMissingFu"></div>
  </div>
  <div class="card" id="retinaAuditCard">
    <div class="card-title"><div class="icon">👁️</div> ตรวจตาเบาหวาน / RETINA Audit</div>
    <div class="filter-bar">
      <span class="filter-label">ปีงบประมาณ:</span>
      <input type="number" id="retinaFy" value="2569" style="width:100px">
      <button class="btn btn-primary btn-sm" onclick="renderRetinaAudit()">คำนวณใหม่</button>
      <button class="btn btn-outline btn-sm" onclick="exportRetinaAuditExcel()">Export รายชื่อ</button>
    </div>
    <div id="retinaAudit"></div>
  </div>
  <div class="card" id="chronicCandidateCard">
    <div class="card-title"><div class="icon">💡</div> ควรพิจารณาขึ้นทะเบียน CHRONIC (มี Diag DM/HT แต่ไม่ได้ขึ้น)</div>
    <div id="chronicCandidates"></div>
  </div>
</div>

<!-- PAGE: LAB -->
<div class="page" id="page-lab">
  <div class="card">
    <div class="card-title"><div class="icon">🧪</div> LABFU Audit</div>
    <div id="labStats"></div>
  </div>
  <div class="card">
    <div class="card-title"><div class="icon">⚠️</div> Outlier Lab Results</div>
    <div id="labOutliers"></div>
  </div>
  <div class="card">
    <div class="card-title"><div class="icon">🔗</div> Lab Code ที่ไม่พบใน Mapping</div>
    <div id="labUnmapped"></div>
  </div>
</div>

<!-- PAGE: MAPPING -->
<div class="page" id="page-mapping">
  <div class="card">
    <div class="card-title"><div class="icon">🔗</div> Lab Code Mapping Table (แก้ไขได้)</div>
    <div class="alert alert-info">📌 ตาราง mapping ใช้สำหรับแปลง Local Lab Code → HDC Standard Code ก่อน Upload เข้า HDC</div>
    <div class="btn-group" style="margin-bottom:12px">
      <button class="btn btn-primary btn-sm" onclick="addMappingRow()">➕ เพิ่ม mapping</button>
      <button class="btn btn-outline btn-sm" onclick="exportMapping()">📥 Export Mapping</button>
    </div>
    <div class="tbl-wrap">
      <table>
        <thead>
          <tr>
            <th>Local Code</th>
            <th>Local Name</th>
            <th>HDC Code</th>
            <th>Category</th>
            <th>Unit</th>
            <th>สถานะ</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody id="mappingTableBody"></tbody>
      </table>
    </div>
  </div>
</div>

<!-- PAGE: AUDIT LOG -->
<div class="page" id="page-audit">
  <div class="card">
    <div class="card-title"><div class="icon">📋</div> Audit Log (ประวัติการแก้ไขทั้งหมด)</div>
    <div class="filter-bar">
      <select id="logFilter" onchange="renderAuditLog()">
        <option value="">ทุก Action</option>
        <option value="AUTO_FIX">Auto-fix</option>
        <option value="MANUAL_EDIT">Manual Edit</option>
        <option value="IGNORE">Ignore</option>
        <option value="RESTORE">Restore</option>
      </select>
      <span class="count-badge" id="logCount">0 รายการ</span>
    </div>
    <div id="auditLogList"></div>
  </div>
</div>

<!-- PAGE: EXPORT -->
<div class="page" id="page-export">
  <div class="card">
    <div class="card-title"><div class="icon">💾</div> Export ไฟล์</div>
    <div class="alert alert-warning">⚠️ ก่อน Export ควรตรวจสอบ Preview Fix และยืนยันการแก้ไขทั้งหมดแล้ว</div>
    <div style="display:flex;flex-direction:column;gap:16px;margin-top:16px">
      <div class="card" style="border:2px solid var(--primary);margin:0">
        <div style="font-weight:700;font-size:15px;margin-bottom:8px;color:var(--primary-dark)">📦 ZIP 43 แฟ้มที่แก้ไขแล้ว</div>
        <div style="font-size:13px;color:var(--text2);margin-bottom:12px">Export ไฟล์ทุกแฟ้มที่มีการแก้ไข พร้อมคงโครงสร้าง column เดิม</div>
        <button class="btn btn-primary" onclick="exportFixedZip()">📦 Export ZIP</button>
      </div>
      <div class="card" style="border:2px solid var(--warning);margin:0">
        <div style="font-weight:700;font-size:15px;margin-bottom:8px;color:#92400e">📊 Excel Error Report</div>
        <div style="font-size:13px;color:var(--text2);margin-bottom:12px">รายงาน Error ทั้งหมดในรูปแบบ Excel (.xlsx)</div>
        <button class="btn btn-warning" onclick="exportErrorExcel()">📊 Export Excel</button>
      </div>
      <div class="card" style="border:2px solid var(--info);margin:0">
        <div style="font-weight:700;font-size:15px;margin-bottom:8px;color:#0369a1">📋 Excel Audit Log</div>
        <div style="font-size:13px;color:var(--text2);margin-bottom:12px">บันทึกการแก้ไขทั้งหมดในรูปแบบ Excel (.xlsx)</div>
        <button class="btn btn-info" onclick="exportAuditExcel()">📋 Export Audit Log</button>
      </div>
    </div>
  </div>
</div>

</div><!-- /container -->

<script>
// ========== GLOBALS ==========
let state = {
  files: {},          // filename -> { header:[], rows:[[]], rawText:string }
  errors: [],
  auditLog: [],
  fixes: {},          // filename -> { rowIdx -> {colIdx -> newVal} }
  originals: {},      // filename -> originalText
  hcode: '11422',
  checked: false,
  charts: {}
};

// ========== LAB MAPPING (Default) ==========
let labMapping = [
  { local:'0531601', localName:'HbA1c', hdc:'0531601', category:'HbA1c', unit:'%', active:true },
  { local:'0531002', localName:'FBS (Glucose)', hdc:'0531001', category:'FBS', unit:'mg/dL', active:true },
  { local:'0531102', localName:'FBS 2hr (OGTT)', hdc:'0531101', category:'FBS', unit:'mg/dL', active:true },
  { local:'0510402', localName:'FBS (ห้อง lab)', hdc:'0531001', category:'FBS', unit:'mg/dL', active:true },
  { local:'0511402', localName:'FBS-2', hdc:'0531001', category:'FBS', unit:'mg/dL', active:true },
  { local:'0541402', localName:'LDL Cholesterol', hdc:'0541401', category:'LDL', unit:'mg/dL', active:true },
  { local:'0621201', localName:'Creatinine', hdc:'0621201', category:'Creatinine', unit:'mg/dL', active:true },
  { local:'0581904', localName:'eGFR (CKD-EPI)', hdc:'0581904', category:'eGFR', unit:'mL/min/1.73m²', active:true },
  { local:'0581902', localName:'eGFR (MDRD)', hdc:'0581902', category:'eGFR', unit:'mL/min/1.73m²', active:true },
  { local:'0583001', localName:'Microalbumin (urine)', hdc:'0583001', category:'Microalbumin', unit:'mg/L', active:true },
  { local:'0440205', localName:'Total Cholesterol', hdc:'0440201', category:'Lipid', unit:'mg/dL', active:true },
  { local:'0440203', localName:'Triglyceride', hdc:'0440203', category:'Lipid', unit:'mg/dL', active:true },
  { local:'0531802', localName:'TG (Triglyceride)', hdc:'0440203', category:'Lipid', unit:'mg/dL', active:true },
  { local:'0743299', localName:'Urine protein', hdc:'0743299', category:'Urine', unit:'-', active:true },
  { local:'0721297', localName:'Urine glucose', hdc:'0721297', category:'Urine', unit:'-', active:true },
  { local:'0746299', localName:'Urine ketone', hdc:'0746299', category:'Urine', unit:'-', active:true },
  { local:'0749200', localName:'Urine dipstick', hdc:'0749200', category:'Urine', unit:'-', active:true },
  { local:'0749202', localName:'Urine albumin', hdc:'0749202', category:'Urine', unit:'mg/g Cr', active:true },
  { local:'0741699', localName:'WBC', hdc:'0741699', category:'CBC', unit:'/mm³', active:true },
  { local:'0741499', localName:'Hb/Hct', hdc:'0741499', category:'CBC', unit:'g/dL', active:true },
];

// ========== UTILITY ==========
function $(id) { return document.getElementById(id); }

function fmtNum(n) { return n?.toLocaleString('th-TH') ?? '—'; }

function fmtDate(s) {
  if (!s) return '—';
  s = String(s).trim();
  if (s.length === 8) return s.slice(0,4)+'-'+s.slice(4,6)+'-'+s.slice(6,8);
  return s;
}

function isValidDate8(s) {
  if (!s || s.length !== 8 || !/^\d{8}$/.test(s)) return false;
  const y = parseInt(s.slice(0,4)), m = parseInt(s.slice(4,6)), d = parseInt(s.slice(6,8));
  if (m < 1 || m > 12 || d < 1 || d > 31) return false;
  return true;
}

function isICD10(s) {
  if (!s) return false;
  return /^[A-Z]\d{2,4}(\.\d{1,2})?$/.test(s.trim().toUpperCase());
}

function severityOrder(s) {
  return { critical:0, high:1, medium:2, low:3 }[s] ?? 4;
}

function escapeHtml(v) {
  return String(v ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
}

function ymdToday() {
  const d = new Date();
  return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;
}

function currentThaiFiscalYear() {
  const d = new Date();
  const buddhistYear = d.getFullYear() + 543;
  return d.getMonth() >= 9 ? buddhistYear + 1 : buddhistYear;
}

function fiscalRange(thaiFy) {
  const adFy = Number(thaiFy) - 543;
  return { start: `${adFy-1}1001`, end: `${adFy}0930` };
}

function addError(sev, module, file, rowNum, pid, cid, seq, field, curVal, sugVal, desc, action='manual') {
  state.errors.push({ sev, module, file, rowNum, pid, cid, seq, field, curVal: String(curVal??''), sugVal: String(sugVal??''), desc, action });
}

function addLog(action, detail, before='', after='') {
  const now = new Date();
  state.auditLog.push({
    time: now.toLocaleString('th-TH'),
    action, detail, before, after
  });
}

function loading(show, text='กำลังประมวลผล...', sub='กรุณารอสักครู่') {
  const el = $('loadingOverlay');
  $('loadingText').textContent = text;
  $('loadingSub').textContent = sub;
  el.classList.toggle('show', show);
}

function showPage(name) {
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  document.querySelectorAll('.navtab').forEach(t => t.classList.remove('active'));
  $('page-'+name).classList.add('active');
  $('tab-'+name).classList.add('active');
}

function openModal(title, body, footer='') {
  $('modalTitle').textContent = title;
  $('modalBody').innerHTML = body;
  $('modalFooter').innerHTML = `<button class="btn btn-outline btn-sm" onclick="closeModal()">ปิด</button>` + footer;
  $('modalBackdrop').classList.add('show');
}
function closeModal() { $('modalBackdrop').classList.remove('show'); }

function setProgress(pct, label, sub='') {
  $('progressFill').style.width = pct + '%';
  $('progressPct').textContent = Math.round(pct) + '%';
  $('progressLabel').textContent = label;
  if (sub) $('statusText').textContent = sub;
}

// ========== ENCODING DETECTION ==========
function detectAndDecode(buffer) {
  // Try UTF-8 first
  try {
    const utf8 = new TextDecoder('utf-8', {fatal:true}).decode(buffer);
    // Verify it has Thai characters or pipe delimiters
    if (utf8.includes('|')) return utf8;
  } catch(e) {}
  // Try TIS-620 / Windows-874
  try {
    return new TextDecoder('windows-874').decode(buffer);
  } catch(e) {}
  return new TextDecoder('utf-8', {fatal:false}).decode(buffer);
}

// ========== FILE PARSING ==========
function parseTxt(text) {
  const lines = text.replace(/\r\n/g,'\n').replace(/\r/g,'\n').split('\n').filter(l => l.trim());
  if (lines.length === 0) return { header:[], rows:[] };
  const header = lines[0].split('|');
  const rows = lines.slice(1).map(l => l.split('|'));
  return { header, rows };
}

function colIdx(file, colName) {
  const f = state.files[file];
  if (!f) return -1;
  return f.header.indexOf(colName);
}

function getVal(file, row, colName) {
  const idx = colIdx(file, colName);
  if (idx < 0) return '';
  return (row[idx] ?? '').trim();
}

function isDmCode(code) {
  return /^E1[0-4]/.test(String(code || '').trim().toUpperCase());
}

function serializeFile(file) {
  const f = state.files[file];
  if (!f) return '';
  const lines = [f.header.join('|'), ...f.rows.map(row => {
    const out = [];
    for (let i = 0; i < f.header.length; i++) out.push(String(row[i] ?? ''));
    return out.join('|');
  })];
  return lines.join('\r\n') + '\r\n';
}

function touchFile(file) {
  if (state.files[file]) state.files[file].rawText = serializeFile(file);
}

function ensureFile(file, header) {
  if (state.files[file]) return state.files[file];
  state.files[file] = { header:[...header], rows:[], rawText:'' };
  state.originals[file] = '';
  touchFile(file);
  renderFileSummary();
  return state.files[file];
}

function setCellValue(file, rowNum, field, value) {
  const f = state.files[file];
  if (!f) throw new Error(`ไม่พบไฟล์ ${file}`);
  const rowIdx = Number(rowNum) - 2;
  if (rowIdx < 0 || rowIdx >= f.rows.length) throw new Error(`row ${rowNum} ไม่สามารถแก้แบบ cell ได้`);
  const c = colIdx(file, field);
  if (c < 0) throw new Error(`ไม่พบ column ${field} ใน ${file}`);
  const before = String(f.rows[rowIdx][c] ?? '');
  f.rows[rowIdx][c] = String(value ?? '');
  if (!state.fixes[file]) state.fixes[file] = {};
  if (!state.fixes[file][rowNum]) state.fixes[file][rowNum] = {};
  state.fixes[file][rowNum][field] = String(value ?? '');
  touchFile(file);
  return before;
}

function appendRow(file, values) {
  const f = state.files[file];
  if (!f) throw new Error(`ไม่พบไฟล์ ${file}`);
  const row = f.header.map(h => String(values[h] ?? ''));
  f.rows.push(row);
  touchFile(file);
  return f.rows.length + 1;
}

function findRowNumByPid(file, pid) {
  const f = state.files[file];
  if (!f || !pid) return 0;
  const idx = f.rows.findIndex(r => getVal(file, r, 'PID') === String(pid));
  return idx >= 0 ? idx + 2 : 0;
}

function personCidByPid(pid) {
  const rowNum = findRowNumByPid('person.txt', pid);
  if (!rowNum) return '';
  return getVal('person.txt', state.files['person.txt'].rows[rowNum - 2], 'CID');
}

function findServiceBySeq(seq) {
  const sf = state.files['service.txt'];
  if (!sf || !seq) return null;
  return sf.rows.find(r => getVal('service.txt', r, 'SEQ') === String(seq)) || null;
}

function latestRowByPid(file, pid, dateField='DATE_SERV') {
  const f = state.files[file];
  if (!f || !pid) return null;
  return f.rows
    .filter(r => getVal(file, r, 'PID') === String(pid))
    .sort((a,b) => getVal(file, b, dateField).localeCompare(getVal(file, a, dateField)))[0] || null;
}

function refreshChecksAfterEdit(keepPage='errors') {
  if (!Object.keys(state.files).length) return;
  state.errors = [];
  checkPerson();
  checkService();
  checkChronic();
  checkChronicFu();
  checkLabFu();
  checkDiagnosisOpd();
  checkProcedureOpd();
  state.checked = true;
  renderDashboard();
  renderErrorTable();
  renderPersonPage();
  renderChronicPage();
  renderRetinaAudit();
  renderLabPage();
  renderMappingTable();
  const files = [...new Set(state.errors.map(e=>e.file))].sort();
  $('filterFile').innerHTML = '<option value="">ทุกไฟล์</option>' + files.map(f=>`<option value="${f}">${f}</option>`).join('');
  $('errBadge').textContent = state.errors.filter(e => e.sev === 'critical' || e.sev === 'high').length;
  $('errBadge').className = 'badge' + (state.errors.some(e=>e.sev==='critical')?' ':' warn');
  showPage(keepPage);
}

// ========== FILE UPLOAD ==========
function handleFileSelect(e) {
  const file = e.target.files[0];
  if (!file) return;
  processZip(file);
}

const dropzone = $('dropzone');
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('drag'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag'));
dropzone.addEventListener('drop', e => {
  e.preventDefault();
  dropzone.classList.remove('drag');
  const file = e.dataTransfer.files[0];
  if (file) processZip(file);
});

async function processZip(file) {
  $('progressWrap').style.display = 'block';
  setProgress(5, 'กำลังเปิด ZIP...', file.name);
  loading(true, 'กำลังเปิด ZIP', file.name);

  try {
    const buf = await file.arrayBuffer();
    const zip = await JSZip.loadAsync(buf);
    const entries = Object.values(zip.files).filter(f => !f.dir && f.name.endsWith('.txt'));
    const total = entries.length;
    let done = 0;

    state.files = {};
    state.originals = {};
    state.fixes = {};

    for (const entry of entries) {
      const fname = entry.name.split('/').pop().toLowerCase();
      const rawBuf = await entry.async('arraybuffer');
      const rawText = detectAndDecode(rawBuf);
      const parsed = parseTxt(rawText);
      state.files[fname] = { ...parsed, rawText };
      state.originals[fname] = rawText;
      done++;
      setProgress(5 + (done/total)*60, `อ่านไฟล์: ${fname}`, `${done}/${total} ไฟล์`);
    }

    // Detect HCODE
    const personFile = state.files['person.txt'];
    if (personFile && personFile.rows.length > 0) {
      state.hcode = personFile.rows[0][0] || '11422';
      $('topHcode').textContent = 'HCODE: ' + state.hcode;
    }

    setProgress(70, 'สร้างสรุปไฟล์...');
    renderFileSummary();
    setProgress(100, 'เสร็จสิ้น ✓', 'พร้อมตรวจสอบ');
    $('fileSummarySection').style.display = 'block';
    loading(false);
    addLog('UPLOAD', `โหลดไฟล์ ZIP สำเร็จ ${total} ไฟล์`, '', file.name);
  } catch(err) {
    loading(false);
    setProgress(0, 'เกิดข้อผิดพลาด', err.message);
    alert('ไม่สามารถเปิดไฟล์ ZIP ได้: ' + err.message);
  }
}

// ========== FILE SUMMARY ==========
function renderFileSummary() {
  const files = Object.entries(state.files);
  const requiredFiles = ['person.txt','service.txt','diagnosis_opd.txt','chronic.txt','chronicfu.txt','labfu.txt','procedure_opd.txt','death.txt','address.txt'];
  
  const totalRows = files.reduce((s,[,f]) => s + f.rows.length, 0);
  const personRows = state.files['person.txt']?.rows.length ?? 0;
  const serviceRows = state.files['service.txt']?.rows.length ?? 0;
  const missingCore = requiredFiles.filter(f => !state.files[f]).length;

  $('fileSummaryKpi').innerHTML = `
    <div class="kpi-card blue"><div class="kpi-val">${files.length}</div><div class="kpi-label">จำนวนไฟล์</div></div>
    <div class="kpi-card green"><div class="kpi-val">${fmtNum(personRows)}</div><div class="kpi-label">PERSON</div></div>
    <div class="kpi-card green"><div class="kpi-val">${fmtNum(serviceRows)}</div><div class="kpi-label">SERVICE visit</div></div>
    <div class="kpi-card ${missingCore>0?'red':'green'}"><div class="kpi-val ${missingCore>0?'red':''}">${missingCore}</div><div class="kpi-label">ไฟล์หลักที่หาย</div></div>
    <div class="kpi-card blue"><div class="kpi-val">${fmtNum(totalRows)}</div><div class="kpi-label">แถวทั้งหมด</div></div>
  `;

  $('fileGrid').innerHTML = files.map(([name, f]) => {
    const isRequired = requiredFiles.includes(name);
    return `<div class="file-item">
      <span class="fi-icon">${isRequired?'📌':'📄'}</span>
      <div>
        <div class="fi-name">${name}</div>
        <div class="fi-info">${fmtNum(f.rows.length)} แถว | ${f.header.length} col</div>
      </div>
      <div class="fi-status"><span class="dot dot-ok"></span></div>
    </div>`;
  }).join('') + requiredFiles.filter(f => !state.files[f]).map(name => `
    <div class="file-item" style="border-color:var(--danger);background:#fff5f5">
      <span class="fi-icon">❌</span>
      <div>
        <div class="fi-name" style="color:var(--danger)">${name}</div>
        <div class="fi-info" style="color:var(--danger)">ไม่พบไฟล์</div>
      </div>
      <div class="fi-status"><span class="dot dot-err"></span></div>
    </div>`).join('');
}

// ========== MAIN CHECK RUNNER ==========
async function runAllChecks() {
  if (!Object.keys(state.files).length) { alert('กรุณาอัปโหลดไฟล์ก่อน'); return; }
  loading(true, 'กำลังตรวจสอบ...', 'โปรดรอ');
  state.errors = [];

  await sleep(50);
  loading(true, 'ตรวจสอบ PERSON...', '');
  checkPerson();

  await sleep(50);
  loading(true, 'ตรวจสอบ SERVICE Linkage...', '');
  checkService();

  await sleep(50);
  loading(true, 'ตรวจสอบ CHRONIC...', '');
  checkChronic();

  await sleep(50);
  loading(true, 'ตรวจสอบ CHRONICFU...', '');
  checkChronicFu();

  await sleep(50);
  loading(true, 'ตรวจสอบ LABFU...', '');
  checkLabFu();

  await sleep(50);
  loading(true, 'ตรวจสอบ DIAGNOSIS_OPD...', '');
  checkDiagnosisOpd();

  await sleep(50);
  loading(true, 'ตรวจสอบ PROCEDURE_OPD...', '');
  checkProcedureOpd();

  await sleep(50);
  loading(true, 'สร้าง Dashboard...', '');
  state.checked = true;
  renderDashboard();
  renderErrorTable();
  renderPersonPage();
  renderChronicPage();
  renderRetinaAudit();
  renderLabPage();
  renderMappingTable();

  $('errBadge').textContent = state.errors.filter(e => e.sev === 'critical' || e.sev === 'high').length;
  $('errBadge').className = 'badge' + (state.errors.some(e=>e.sev==='critical')?' ':' warn');

  // Populate filter selects
  const files = [...new Set(state.errors.map(e=>e.file))].sort();
  $('filterFile').innerHTML = '<option value="">ทุกไฟล์</option>' + files.map(f=>`<option value="${f}">${f}</option>`).join('');

  addLog('CHECK', `ตรวจสอบเสร็จสิ้น พบ ${state.errors.length} รายการ error`);
  loading(false);
  showPage('dashboard');
}

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// ========== CHECK: PERSON ==========
function checkPerson() {
  const f = state.files['person.txt'];
  if (!f) { addError('critical','PERSON','person.txt',0,'','','','','','','ไม่พบไฟล์ person.txt'); return; }

  const seen_pid = new Map(), seen_cid = new Map();
  const typeareas = { 1:0, 2:0, 3:0, 4:0, 5:0, other:0 };

  f.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('person.txt', row, 'PID');
    const cid = getVal('person.txt', row, 'CID');
    const typearea = getVal('person.txt', row, 'TYPEAREA');
    const discharge = getVal('person.txt', row, 'DISCHARGE');
    const hospcode = getVal('person.txt', row, 'HOSPCODE');

    if (!pid) addError('critical','PERSON','person.txt',ri,pid,cid,'','PID','','','PID ว่างเปล่า');
    if (!cid) addError('high','PERSON','person.txt',ri,pid,cid,'','CID','','','CID ว่างเปล่า');
    if (cid && cid.length !== 13) addError('medium','PERSON','person.txt',ri,pid,cid,'','CID',cid,'','CID ไม่ใช่ 13 หลัก');
    if (pid && seen_pid.has(pid)) addError('critical','PERSON','person.txt',ri,pid,cid,'','PID',pid,'','PID ซ้ำกับ row '+seen_pid.get(pid),'manual');
    else if (pid) seen_pid.set(pid, ri);
    if (cid && seen_cid.has(cid)) addError('critical','PERSON','person.txt',ri,pid,cid,'','CID',cid,'','CID ซ้ำกับ row '+seen_cid.get(cid),'manual');
    else if (cid) seen_cid.set(cid, ri);
    if (!typearea) addError('high','PERSON','person.txt',ri,pid,cid,'','TYPEAREA','','4','TYPEAREA ว่างเปล่า','auto');
    else if (!['1','2','3','4','5'].includes(typearea)) addError('high','PERSON','person.txt',ri,pid,cid,'','TYPEAREA',typearea,'4','TYPEAREA ไม่ใช่ 1-5','auto');
    if (hospcode !== state.hcode) addError('medium','PERSON','person.txt',ri,pid,cid,'','HOSPCODE',hospcode,state.hcode,'HOSPCODE ไม่ตรง');

    const ta = parseInt(typearea);
    if ([1,2,3,4,5].includes(ta)) typeareas[ta]++;
    else typeareas.other++;
  });

  // Check DEATH cross-reference
  const deathFile = state.files['death.txt'];
  const personPidSet = new Map(f.rows.map(r => [getVal('person.txt',r,'PID'), r]));
  if (deathFile) {
    deathFile.rows.forEach((row, i) => {
      const pid = getVal('death.txt', row, 'PID');
      if (pid && personPidSet.has(pid)) {
        const prow = personPidSet.get(pid);
        const disc = getVal('person.txt', prow, 'DISCHARGE');
        if (!disc || disc === '0' || disc === '') {
          addError('high','PERSON','person.txt',0,pid,'','','DISCHARGE','','4','มีใน DEATH แต่ PERSON ยังไม่ update DISCHARGE=4','auto');
        }
      }
    });
  }

  // Warnings for unusual typearea
  const total = f.rows.length;
  if (typeareas[1] < total * 0.1) addError('medium','PERSON','person.txt',0,'','','','TYPEAREA','TYPEAREA 1='+typeareas[1],'','TYPEAREA 1 (ทะเบียนในเขตและอยู่จริง) น้อยผิดปกติ (<10%)',  'ignore');
  if (typeareas[4] > total * 0.8) addError('medium','PERSON','person.txt',0,'','','','TYPEAREA','TYPEAREA 4='+typeareas[4],'','TYPEAREA 4 (นอกเขตแต่มารับบริการ) สูงผิดปกติ (>80%)', 'ignore');

  // Store for page render
  state._personTypeareas = typeareas;
  state._personTotal = f.rows.length;
}

// ========== CHECK: SERVICE ==========
function checkService() {
  const sf = state.files['service.txt'];
  const pf = state.files['person.txt'];
  if (!sf || !pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const seen_seq = new Map();
  const diagSeqs = new Set(state.files['diagnosis_opd.txt']?.rows.map(r => getVal('diagnosis_opd.txt',r,'SEQ')) ?? []);
  const procSeqs = new Set(state.files['procedure_opd.txt']?.rows.map(r => getVal('procedure_opd.txt',r,'SEQ')) ?? []);
  
  sf.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('service.txt', row, 'PID');
    const seq = getVal('service.txt', row, 'SEQ');
    const date = getVal('service.txt', row, 'DATE_SERV');
    const hospcode = getVal('service.txt', row, 'HOSPCODE');

    if (pid && !personPids.has(pid)) addError('high','SERVICE','service.txt',ri,pid,'',seq,'PID',pid,'','PID ไม่พบใน PERSON');
    if (!seq) addError('critical','SERVICE','service.txt',ri,pid,'',seq,'SEQ','','','SEQ ว่างเปล่า');
    if (seq && seen_seq.has(seq)) addError('critical','SERVICE','service.txt',ri,pid,'',seq,'SEQ',seq,'','SEQ ซ้ำกับ row '+seen_seq.get(seq),'manual');
    else if (seq) seen_seq.set(seq, ri);
    if (date && !isValidDate8(date)) addError('medium','SERVICE','service.txt',ri,pid,'',seq,'DATE_SERV',date,'YYYYMMDD','รูปแบบวันที่ผิด ต้องเป็น YYYYMMDD');
    if (hospcode !== state.hcode) addError('medium','SERVICE','service.txt',ri,pid,'',seq,'HOSPCODE',hospcode,state.hcode,'HOSPCODE ไม่ตรง');
  });

  // SERVICE SEQ ที่ไม่มี DIAG
  const serviceSeqs = new Set(sf.rows.map(r => getVal('service.txt',r,'SEQ')).filter(Boolean));
  const diagSvc = state.files['diagnosis_opd.txt'];
  if (diagSvc) {
    const seqsWithDiag = new Set(diagSvc.rows.map(r => getVal('diagnosis_opd.txt',r,'SEQ')).filter(Boolean));
    let noDiagCount = 0;
    serviceSeqs.forEach(seq => { if (!seqsWithDiag.has(seq)) noDiagCount++; });
    if (noDiagCount > 0) addError('high','SERVICE','service.txt',0,'','','','SEQ','','',`SERVICE ${noDiagCount} visit ไม่มี DIAGNOSIS_OPD`,'ignore');
  }
  state._serviceSeqs = serviceSeqs;
}

// ========== CHECK: CHRONIC ==========
function checkChronic() {
  const cf = state.files['chronic.txt'];
  const pf = state.files['person.txt'];
  if (!cf || !pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const chronicCodes = {
    DM: /^E1[0-4]/,
    HT: /^I1[0-5]/,
    CKD: /^N18/,
    COPD: /^J44/,
    ASTHMA: /^J45/,
    STROKE: /^I6[0-9]/,
    IHD: /^I2[0-5]/
  };
  const chronicPids = new Set();

  cf.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('chronic.txt', row, 'PID');
    const cid = getVal('chronic.txt', row, 'CID');
    const chronic = getVal('chronic.txt', row, 'CHRONIC');
    const dateDiag = getVal('chronic.txt', row, 'DATE_DIAG');
    const hospcode = getVal('chronic.txt', row, 'HOSPCODE');

    if (pid) chronicPids.add(pid);
    if (pid && !personPids.has(pid)) addError('high','CHRONIC','chronic.txt',ri,pid,cid,'','PID',pid,'','PID ไม่พบใน PERSON');
    if (!chronic) addError('high','CHRONIC','chronic.txt',ri,pid,cid,'','CHRONIC','','','CHRONIC code ว่างเปล่า');
    else if (!isICD10(chronic)) addError('medium','CHRONIC','chronic.txt',ri,pid,cid,'','CHRONIC',chronic,'','CHRONIC code ไม่ใช่ ICD-10');
    if (dateDiag && !isValidDate8(dateDiag)) addError('medium','CHRONIC','chronic.txt',ri,pid,cid,'','DATE_DIAG',dateDiag,'YYYYMMDD','รูปแบบวันที่ผิด');
    if (hospcode !== state.hcode) addError('low','CHRONIC','chronic.txt',ri,pid,cid,'','HOSPCODE',hospcode,state.hcode,'HOSPCODE ไม่ตรง');
  });

  state._chronicPids = chronicPids;

  // Check Diag DM/HT without CHRONIC
  const diagFile = state.files['diagnosis_opd.txt'];
  if (diagFile) {
    const dmhtPids = new Set();
    const candidateInfo = {};
    diagFile.rows.forEach(row => {
      const diag = getVal('diagnosis_opd.txt', row, 'DIAGCODE');
      const pid = getVal('diagnosis_opd.txt', row, 'PID');
      if (diag && (chronicCodes.DM.test(diag) || chronicCodes.HT.test(diag))) {
        if (!chronicPids.has(pid)) {
          dmhtPids.add(pid);
          const dateServ = getVal('diagnosis_opd.txt', row, 'DATE_SERV');
          const current = candidateInfo[pid];
          if (!current || dateServ > current.dateServ) {
            candidateInfo[pid] = { pid, chronic: diag, dateServ, cid: getVal('diagnosis_opd.txt', row, 'CID'), seq: getVal('diagnosis_opd.txt', row, 'SEQ') };
          }
        }
      }
    });
    state._candidatePids = dmhtPids;
    state._candidateChronicInfo = candidateInfo;
    if (dmhtPids.size > 0) addError('medium','CHRONIC','diagnosis_opd.txt',0,'','','','CHRONIC','','',`มี ${dmhtPids.size} คนที่มี Diag DM/HT แต่ไม่มีใน CHRONIC ควรพิจารณาขึ้นทะเบียน`,'ignore');
  }
  state._chronicCodes = chronicCodes;
}

// ========== CHECK: CHRONICFU ==========
function checkChronicFu() {
  const cf = state.files['chronicfu.txt'];
  const chronicPids = state._chronicPids || new Set();
  const pf = state.files['person.txt'];
  if (!pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const fuPids = new Set();
  const requiredFields = ['WEIGHT','HEIGHT','WAIST_CM','SBP','DBP','FOOT','RETINA','SMOKING','ALCOHOL','PROVIDER']
    .filter(field => colIdx('chronicfu.txt', field) >= 0);

  if (cf) {
    cf.rows.forEach((row, i) => {
      const ri = i + 2;
      const pid = getVal('chronicfu.txt', row, 'PID');
      const seq = getVal('chronicfu.txt', row, 'SEQ');
      const cid = getVal('chronicfu.txt', row, 'CID');
      const date = getVal('chronicfu.txt', row, 'DATE_SERV');
      const hospcode = getVal('chronicfu.txt', row, 'HOSPCODE');

      if (pid) fuPids.add(pid);
      if (pid && !personPids.has(pid)) addError('high','CHRONICFU','chronicfu.txt',ri,pid,cid,seq,'PID',pid,'','PID ไม่พบใน PERSON');
      if (date && !isValidDate8(date)) addError('medium','CHRONICFU','chronicfu.txt',ri,pid,cid,seq,'DATE_SERV',date,'YYYYMMDD','รูปแบบวันที่ผิด');
      if (hospcode !== state.hcode) addError('low','CHRONICFU','chronicfu.txt',ri,pid,cid,seq,'HOSPCODE',hospcode,state.hcode,'HOSPCODE ไม่ตรง');
      
      requiredFields.forEach(field => {
        const val = getVal('chronicfu.txt', row, field);
        if (!val) addError('low','CHRONICFU','chronicfu.txt',ri,pid,cid,seq,field,'','',`CHRONICFU ไม่มีค่า ${field}`);
      });
    });
  }

  // CHRONIC ที่ไม่มี FU
  const noFuPids = [...chronicPids].filter(pid => !fuPids.has(pid));
  state._noFuPids = noFuPids;
  if (noFuPids.length > 0) addError('high','CHRONICFU','chronicfu.txt',0,'','','','FU','','',`CHRONIC ${noFuPids.length} ราย ไม่มี CHRONICFU record`,'auto');
  state._fuPids = fuPids;
}

// ========== CHECK: LABFU ==========
function checkLabFu() {
  const lf = state.files['labfu.txt'];
  const pf = state.files['person.txt'];
  if (!lf || !pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const mappedCodes = new Set(labMapping.map(m => m.local));
  const unmappedCodes = new Map();
  const outliers = [];

  const outlierRules = {
    HbA1c: { codes: new Set(['0531601']), min:0, max:20 },
    FBS:   { codes: new Set(['0531001','0531002','0531102','0510402','0511402']), min:0, max:1000 },
    LDL:   { codes: new Set(['0541401','0541402']), min:0, max:500 },
    Creatinine: { codes: new Set(['0621201']), min:0, max:20 },
  };

  lf.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('labfu.txt', row, 'PID');
    const cid = getVal('labfu.txt', row, 'CID');
    const seq = getVal('labfu.txt', row, 'SEQ');
    const labtest = getVal('labfu.txt', row, 'LABTEST');
    const labresult = getVal('labfu.txt', row, 'LABRESULT');
    const date = getVal('labfu.txt', row, 'DATE_SERV');
    const hospcode = getVal('labfu.txt', row, 'HOSPCODE');

    if (pid && !personPids.has(pid)) addError('high','LABFU','labfu.txt',ri,pid,cid,seq,'PID',pid,'','PID ไม่พบใน PERSON');
    if (!labtest) addError('medium','LABFU','labfu.txt',ri,pid,cid,seq,'LABTEST','','','LABTEST ว่างเปล่า');
    else if (!mappedCodes.has(labtest)) {
      unmappedCodes.set(labtest, (unmappedCodes.get(labtest)||0) + 1);
      addError('low','LABFU','labfu.txt',ri,pid,cid,seq,'LABTEST',labtest,'','LABTEST code ไม่พบใน mapping table','auto');
    }
    if (!labresult) addError('medium','LABFU','labfu.txt',ri,pid,cid,seq,'LABRESULT','','','LABRESULT ว่างเปล่า');
    else {
      // Clean result (remove non-numeric suffix like [Stage 2])
      const numResult = parseFloat(labresult.replace(/[^\d.]/g,''));
      for (const [cat, rule] of Object.entries(outlierRules)) {
        if (rule.codes.has(labtest) && !isNaN(numResult)) {
          if (numResult < rule.min || numResult > rule.max) {
            outliers.push({ ri, pid, cid, seq, labtest, labresult, cat });
            addError('medium','LABFU','labfu.txt',ri,pid,cid,seq,'LABRESULT',labresult,'','ค่า '+cat+' outlier ('+numResult+'): ตรวจสอบก่อน Upload','ignore');
          }
        }
      }
    }
    if (date && !isValidDate8(date)) addError('medium','LABFU','labfu.txt',ri,pid,cid,seq,'DATE_SERV',date,'YYYYMMDD','รูปแบบวันที่ผิด');
    if (hospcode !== state.hcode) addError('low','LABFU','labfu.txt',ri,pid,cid,seq,'HOSPCODE',hospcode,state.hcode,'HOSPCODE ไม่ตรง');
  });

  state._unmappedLabCodes = unmappedCodes;
  state._labOutliers = outliers;
}

// ========== CHECK: DIAGNOSIS_OPD ==========
function checkDiagnosisOpd() {
  const df = state.files['diagnosis_opd.txt'];
  const pf = state.files['person.txt'];
  if (!df || !pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const serviceSeqs = state._serviceSeqs || new Set();
  const seqDiagTypes = new Map(); // seq -> Set of diagtypes

  df.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('diagnosis_opd.txt', row, 'PID');
    const seq = getVal('diagnosis_opd.txt', row, 'SEQ');
    const cid = getVal('diagnosis_opd.txt', row, 'CID');
    const diag = getVal('diagnosis_opd.txt', row, 'DIAGCODE');
    const dtype = getVal('diagnosis_opd.txt', row, 'DIAGTYPE');

    if (pid && !personPids.has(pid)) addError('high','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'PID',pid,'','PID ไม่พบใน PERSON');
    if (seq && !serviceSeqs.has(seq)) addError('medium','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'SEQ',seq,'','SEQ ไม่พบใน SERVICE');
    if (!diag) addError('high','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'DIAGCODE','','','DIAGCODE ว่างเปล่า');
    else if (!isICD10(diag.replace('.','').trim())) addError('medium','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'DIAGCODE',diag,'','DIAGCODE ไม่ใช่รูปแบบ ICD-10');
    if (!dtype) addError('medium','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'DIAGTYPE','','1','DIAGTYPE ว่างเปล่า');
    else if (!['1','2','3','4','5'].includes(dtype)) addError('medium','DIAGNOSIS_OPD','diagnosis_opd.txt',ri,pid,cid,seq,'DIAGTYPE',dtype,'','DIAGTYPE ไม่ใช่ 1-5');

    if (seq) {
      if (!seqDiagTypes.has(seq)) seqDiagTypes.set(seq, new Set());
      seqDiagTypes.get(seq).add(dtype);
    }
  });

  // Visit ไม่มี primary diag
  let noPrimary = 0;
  seqDiagTypes.forEach((types, seq) => { if (!types.has('1')) noPrimary++; });
  if (noPrimary > 0) addError('high','DIAGNOSIS_OPD','diagnosis_opd.txt',0,'','','','DIAGTYPE','','',''+noPrimary+' visit ไม่มี Primary Diagnosis (DIAGTYPE=1)','ignore');
}

// ========== CHECK: PROCEDURE_OPD ==========
function checkProcedureOpd() {
  const pf2 = state.files['procedure_opd.txt'];
  const pf = state.files['person.txt'];
  if (!pf2 || !pf) return;

  const personPids = new Set(pf.rows.map(r => getVal('person.txt',r,'PID')));
  const serviceSeqs = state._serviceSeqs || new Set();

  pf2.rows.forEach((row, i) => {
    const ri = i + 2;
    const pid = getVal('procedure_opd.txt', row, 'PID');
    const seq = getVal('procedure_opd.txt', row, 'SEQ');
    const cid = getVal('procedure_opd.txt', row, 'CID');
    const proc = getVal('procedure_opd.txt', row, 'PROCEDCODE');
    const date = getVal('procedure_opd.txt', row, 'DATE_SERV');

    if (pid && !personPids.has(pid)) addError('high','PROCEDURE_OPD','procedure_opd.txt',ri,pid,cid,seq,'PID',pid,'','PID ไม่พบใน PERSON');
    if (seq && !serviceSeqs.has(seq)) addError('medium','PROCEDURE_OPD','procedure_opd.txt',ri,pid,cid,seq,'SEQ',seq,'','SEQ ไม่พบใน SERVICE');
    if (!proc) addError('medium','PROCEDURE_OPD','procedure_opd.txt',ri,pid,cid,seq,'PROCEDCODE','','','PROCEDCODE ว่างเปล่า');
    if (date && !isValidDate8(date)) addError('medium','PROCEDURE_OPD','procedure_opd.txt',ri,pid,cid,seq,'DATE_SERV',date,'YYYYMMDD','รูปแบบวันที่ผิด');
  });
}

// ========== DASHBOARD ==========
function calcScore() {
  const total = state.errors.length;
  if (total === 0) return 100;
  const weights = { critical: 10, high: 5, medium: 2, low: 1 };
  const severity_sum = state.errors.reduce((s,e) => s + (weights[e.sev]||0), 0);
  const max_penalty = 500;
  return Math.max(0, Math.round(100 - Math.min(severity_sum, max_penalty) / max_penalty * 100));
}

function renderDashboard() {
  $('dashEmpty').style.display = 'none';
  $('dashContent').style.display = 'block';

  const score = calcScore();
  const scoreColor = score >= 80 ? '#16a34a' : score >= 60 ? '#d97706' : '#dc2626';

  // Score ring
  if (state.charts.score) state.charts.score.destroy();
  state.charts.score = new Chart($('scoreChart'), {
    type: 'doughnut',
    data: { datasets: [{ data: [score, 100-score], backgroundColor: [scoreColor,'#e5e7eb'], borderWidth: 0 }] },
    options: { cutout:'75%', plugins:{legend:{display:false}}, animation:{duration:800} }
  });
  $('scoreNum').textContent = score;
  $('scoreNum').style.color = scoreColor;

  const cnt = sev => state.errors.filter(e=>e.sev===sev).length;
  const personFile = state.files['person.txt'];
  const chronicFile = state.files['chronic.txt'];
  $('dashKpi').innerHTML = `
    <div class="kpi-card red"><div class="kpi-val red">${cnt('critical')}</div><div class="kpi-label">Critical Error</div></div>
    <div class="kpi-card yellow"><div class="kpi-val yellow">${cnt('high')}</div><div class="kpi-label">High Error</div></div>
    <div class="kpi-card blue"><div class="kpi-val">${cnt('medium')}</div><div class="kpi-label">Medium Error</div></div>
    <div class="kpi-card green"><div class="kpi-val">${cnt('low')}</div><div class="kpi-label">Low / Info</div></div>
    <div class="kpi-card purple"><div class="kpi-val">${fmtNum(personFile?.rows.length)}</div><div class="kpi-label">PERSON ทั้งหมด</div></div>
    <div class="kpi-card green"><div class="kpi-val">${fmtNum(chronicFile?.rows.length)}</div><div class="kpi-label">CHRONIC</div></div>
    <div class="kpi-card yellow"><div class="kpi-val">${state._noFuPids?.length ?? 0}</div><div class="kpi-label">CHRONIC ไม่มี FU</div></div>
    <div class="kpi-card blue"><div class="kpi-val">${state._unmappedLabCodes?.size ?? 0}</div><div class="kpi-label">Lab Code ไม่ map</div></div>
  `;

  // Severity chart
  if (state.charts.sev) state.charts.sev.destroy();
  state.charts.sev = new Chart($('sevChart'), {
    type: 'bar',
    data: {
      labels: ['Critical','High','Medium','Low'],
      datasets: [{ data: [cnt('critical'),cnt('high'),cnt('medium'),cnt('low')],
        backgroundColor: ['#dc2626','#f59e0b','#fef08a','#93c5fd'], borderRadius: 6 }]
    },
    options: { plugins:{legend:{display:false}}, scales:{y:{beginAtZero:true,grid:{color:'#e5e7eb'}}}, animation:{duration:600} }
  });

  // Module chart
  const modules = ['PERSON','SERVICE','CHRONIC','CHRONICFU','LABFU','DIAGNOSIS_OPD','PROCEDURE_OPD'];
  const modCounts = modules.map(m => state.errors.filter(e=>e.module===m).length);
  if (state.charts.module) state.charts.module.destroy();
  state.charts.module = new Chart($('moduleChart'), {
    type: 'bar',
    data: {
      labels: modules,
      datasets: [{ data: modCounts, backgroundColor: '#0d6e4d', borderRadius: 6 }]
    },
    options: { indexAxis:'y', plugins:{legend:{display:false}}, scales:{x:{beginAtZero:true,grid:{color:'#e5e7eb'}}}, animation:{duration:600} }
  });

  // TYPEAREA chart
  const ta = state._personTypeareas || {};
  if (state.charts.typearea) state.charts.typearea.destroy();
  state.charts.typearea = new Chart($('typeareaChart'), {
    type: 'pie',
    data: {
      labels: ['Type 1 ทะเบียนในเขต/อยู่จริง','Type 2 ทะเบียนในเขต/ตัวไม่อยู่','Type 3 อยู่จริง/ทะเบียนนอกเขต','Type 4 นอกเขตมารับบริการ','Type 5 อื่นๆ'],
      datasets: [{ data: [ta[1]||0,ta[2]||0,ta[3]||0,ta[4]||0,ta[5]||0],
        backgroundColor: ['#0d6e4d','#00b4d8','#f59e0b','#dc2626','#8b5cf6'] }]
    },
    options: { plugins:{legend:{position:'right'}}, animation:{duration:600} }
  });

  // Top 10 error types
  const errGroups = {};
  state.errors.forEach(e => {
    const key = e.module + ' - ' + e.field;
    errGroups[key] = (errGroups[key]||0) + 1;
  });
  const top10 = Object.entries(errGroups).sort((a,b)=>b[1]-a[1]).slice(0,10);
  if (state.charts.top10) state.charts.top10.destroy();
  state.charts.top10 = new Chart($('top10Chart'), {
    type: 'bar',
    data: {
      labels: top10.map(([k])=>k.length>25?k.slice(0,25)+'…':k),
      datasets: [{ data: top10.map(([,v])=>v), backgroundColor: '#f59e0b', borderRadius: 4 }]
    },
    options: { indexAxis:'y', plugins:{legend:{display:false}}, scales:{x:{beginAtZero:true}}, animation:{duration:600} }
  });

  // Alerts
  const alerts = [];
  if (cnt('critical') > 0) alerts.push(`<div class="alert alert-danger">🚨 พบ <strong>${cnt('critical')} Critical Error</strong> — ต้องแก้ไขก่อน Upload HDC</div>`);
  if (state._noFuPids?.length > 0) alerts.push(`<div class="alert alert-warning">⚠️ CHRONIC ${state._noFuPids.length} รายไม่มี CHRONICFU — จะส่งผลต่อตัวชี้วัด NCD HDC</div>`);
  if (state._candidatePids?.size > 0) alerts.push(`<div class="alert alert-info">💡 พบผู้ป่วย DM/HT ${state._candidatePids.size} รายที่ยังไม่ขึ้นทะเบียน CHRONIC</div>`);
  if (score >= 85) alerts.push(`<div class="alert alert-success">✅ คะแนน HDC Readiness สูง (${score}/100) — พร้อม Upload</div>`);
  $('dashAlerts').innerHTML = alerts.join('') || '<div class="alert alert-success">✅ ไม่พบ Alert สำคัญ</div>';
}

// ========== ERROR TABLE ==========
function renderErrorTable() {
  const sev = $('filterSev').value;
  const file = $('filterFile').value;
  const module = $('filterModule').value;
  const search = ($('filterSearch').value || '').toLowerCase();

  const filtered = state.errors.filter(e => {
    if (sev && e.sev !== sev) return false;
    if (file && e.file !== file) return false;
    if (module && e.module !== module) return false;
    if (search && !`${e.pid}${e.cid}${e.seq}${e.desc}`.toLowerCase().includes(search)) return false;
    return true;
  }).sort((a,b) => severityOrder(a.sev) - severityOrder(b.sev));

  $('errCount').textContent = fmtNum(filtered.length) + ' รายการ';
  const sevBadge = s => `<span class="sev sev-${s}">${{critical:'Critical',high:'High',medium:'Medium',low:'Low',info:'Info'}[s]||s}</span>`;

  $('errTableBody').innerHTML = filtered.slice(0,500).map((e,i) => {
    const idx = state.errors.indexOf(e);
    const canManualEdit = e.file && e.rowNum > 1 && e.field && state.files[e.file] && colIdx(e.file, e.field) >= 0;
    return `
    <tr>
      <td class="mono">${i+1}</td>
      <td>${sevBadge(e.sev)}</td>
      <td><span style="font-weight:600;font-size:12px">${escapeHtml(e.module)}</span></td>
      <td class="mono">${e.rowNum||'-'}</td>
      <td class="mono">${escapeHtml(e.pid)||'-'}</td>
      <td class="mono">${escapeHtml(e.cid)||'-'}</td>
      <td class="mono">${escapeHtml(e.seq)||'-'}</td>
      <td><strong>${escapeHtml(e.field)}</strong></td>
      <td class="mono" style="color:var(--danger)">${escapeHtml(e.curVal)||'-'}</td>
      <td class="mono" style="color:var(--success)">${escapeHtml(e.sugVal)||'-'}</td>
      <td style="max-width:250px;font-size:12px">${escapeHtml(e.desc)}</td>
      <td>
        <div class="action-stack">
          ${e.action==='auto'?`<button class="btn btn-success btn-sm" onclick="applyFix(${idx})">Fix</button>`:''}
          ${canManualEdit?`<button class="btn btn-primary btn-sm" onclick="openManualEdit(${idx})">แก้ไข</button>`:''}
          ${e.module==='CHRONICFU' && e.field==='FU' && e.pid?`<button class="btn btn-warning btn-sm" onclick="openChronicFuEditor('${escapeHtml(e.pid)}')">เพิ่ม FU</button>`:''}
          ${e.action==='manual' && !canManualEdit?`<span class="sev sev-high" style="font-size:11px">Manual</span>`:''}
          <button class="btn btn-outline btn-sm" onclick="ignoreError(${idx})">ข้าม</button>
        </div>
      </td>
    </tr>`;
  }).join('') + (filtered.length > 500 ? `<tr><td colspan="12" style="text-align:center;padding:12px;color:var(--text2);font-style:italic">แสดงเพียง 500 รายการแรก (ทั้งหมด ${fmtNum(filtered.length)} รายการ)</td></tr>` : '');
}
function ignoreError(idx) {
  const e = state.errors[idx];
  if (!e) return;
  addLog('IGNORE', `ละเว้น error: ${e.module} ${e.field}`, e.curVal, '');
  state.errors.splice(idx, 1);
  renderErrorTable();
}

function openManualEdit(idx) {
  const e = state.errors[idx];
  if (!e || !state.files[e.file] || e.rowNum < 2) return;
  const fieldOptions = state.files[e.file].header.map(h => `<option value="${escapeHtml(h)}" ${h===e.field?'selected':''}>${escapeHtml(h)}</option>`).join('');
  const current = getVal(e.file, state.files[e.file].rows[e.rowNum - 2], e.field);
  const body = `
    <div class="alert alert-info">แก้ข้อมูลในไฟล์ ${escapeHtml(e.file)} row ${e.rowNum} แล้วระบบจะตรวจซ้ำและนำค่าใหม่ไปใช้ตอน Export ZIP</div>
    <div class="edit-grid">
      <label>File / Row</label><div class="mono">${escapeHtml(e.file)} / ${e.rowNum}</div>
      <label>PID / CID / SEQ</label><div class="mono">${escapeHtml(e.pid)||'-'} / ${escapeHtml(e.cid)||'-'} / ${escapeHtml(e.seq)||'-'}</div>
      <label>Field</label><select id="manualField" onchange="loadManualFieldValue(${idx})">${fieldOptions}</select>
      <label>Current</label><input id="manualCurrent" class="mono" value="${escapeHtml(current)}" readonly>
      <label>New value</label><input id="manualValue" class="mono" value="${escapeHtml(e.sugVal || current)}" autofocus>
    </div>
    <div class="edit-help">TYPEAREA: 1=ทะเบียนในเขตและอยู่จริง, 2=ทะเบียนในเขตแต่ตัวไม่อยู่, 3=อยู่จริงในเขตแต่ทะเบียนนอกเขต, 4=นอกเขตแต่มารับบริการ, 5=อื่นๆ; CID ต้องเป็นเลข 13 หลัก</div>`;
  openModal('Manual Edit', body, `<button class="btn btn-primary btn-sm" onclick="saveManualEdit(${idx})">บันทึกและตรวจซ้ำ</button>`);
}

function loadManualFieldValue(idx) {
  const e = state.errors[idx];
  if (!e) return;
  const field = $('manualField').value;
  const current = getVal(e.file, state.files[e.file].rows[e.rowNum - 2], field);
  $('manualCurrent').value = current;
  $('manualValue').value = current;
}

function saveManualEdit(idx) {
  const e = state.errors[idx];
  if (!e) return;
  const field = $('manualField').value;
  const value = $('manualValue').value.trim();
  const before = setCellValue(e.file, e.rowNum, field, value);
  addLog('MANUAL_EDIT', `แก้ ${e.file} row ${e.rowNum} ${field}`, before, value);
  closeModal();
  refreshChecksAfterEdit('errors');
  renderAuditLog();
}

function locateFixRow(e) {
  if (!e) return 0;
  if (e.rowNum > 1) return e.rowNum;
  if (e.file === 'person.txt' && e.pid) return findRowNumByPid('person.txt', e.pid);
  return 0;
}

function applyFix(idx) {
  const e = state.errors[idx];
  if (!e || !e.sugVal) return;
  const rowNum = locateFixRow(e);
  if (!rowNum || !state.files[e.file] || colIdx(e.file, e.field) < 0) {
    if (e.module === 'CHRONICFU' && e.field === 'FU' && e.pid) openChronicFuEditor(e.pid);
    else alert('รายการนี้ต้องแก้แบบ manual หรือเพิ่ม record ใหม่');
    return;
  }
  const before = setCellValue(e.file, rowNum, e.field, e.sugVal);
  addLog('AUTO_FIX', `Auto-fix: ${e.file} row ${rowNum} ${e.field}`, before, e.sugVal);
  refreshChecksAfterEdit('errors');
  renderAuditLog();
}

function autoFixSafe() {
  const fixable = state.errors.filter(e => e.action === 'auto' && e.sugVal);
  if (!fixable.length) { alert('ไม่มี error ที่สามารถ Auto-fix ได้อย่างปลอดภัย'); return; }
  if (!confirm(`จะ Auto-fix ${fixable.length} รายการ ตรวจสอบ Preview ก่อนยืนยัน?\n\nรายการจะถูก log ใน Audit Log`)) return;
  let fixedCount = 0;
  fixable.forEach(e => {
    const rowNum = locateFixRow(e);
    if (!rowNum || !state.files[e.file] || colIdx(e.file, e.field) < 0) return;
    const before = setCellValue(e.file, rowNum, e.field, e.sugVal);
    addLog('AUTO_FIX', `Auto-fix: ${e.file} row ${rowNum} ${e.field}`, before, e.sugVal);
    fixedCount++;
  });
  refreshChecksAfterEdit('errors');
  renderAuditLog();
  alert(`✅ Auto-fix เสร็จสิ้น ${fixedCount} รายการ\nตรวจสอบ Audit Log เพื่อดูรายละเอียด`);
}

function previewFixes() {
  const fixable = state.errors.filter(e => e.action === 'auto' && e.sugVal);
  if (!fixable.length) { openModal('Preview Fix', '<div class="alert alert-success">ไม่มีรายการที่ auto-fix ได้</div>'); return; }
  const rows = fixable.slice(0,50).map(e => `
    <div class="fix-row">
      <span style="font-weight:600;min-width:120px;font-size:12px">${e.module} / ${e.field}</span>
      <span class="fix-from">${e.curVal||'(ว่าง)'}</span>
      <span style="color:var(--text2)">→</span>
      <span class="fix-to">${e.sugVal}</span>
      <span style="font-size:11px;color:var(--text2)">(row ${e.rowNum})</span>
    </div>`).join('');
  openModal('Preview Auto-fix ('+fixable.length+' รายการ)',
    `<div class="fix-preview">${rows}${fixable.length>50?`<div style="text-align:center;padding:8px;color:var(--text2)">...และอีก ${fixable.length-50} รายการ</div>`:''}</div>`,
    `<button class="btn btn-success btn-sm" onclick="autoFixSafe();closeModal()">✅ Apply ทั้งหมด</button>`
  );
}

// ========== PERSON PAGE ==========
function renderPersonPage() {
  const ta = state._personTypeareas || {};
  const total = state._personTotal || 0;
  const pct = n => total ? Math.round(n/total*100) : 0;

  $('personStats').innerHTML = `
    <div class="kpi-grid">
      <div class="kpi-card blue"><div class="kpi-val">${fmtNum(total)}</div><div class="kpi-label">PERSON ทั้งหมด</div></div>
      <div class="kpi-card red"><div class="kpi-val red">${state.errors.filter(e=>e.module==='PERSON'&&e.sev==='critical').length}</div><div class="kpi-label">Critical Error</div></div>
      <div class="kpi-card yellow"><div class="kpi-val yellow">${state.errors.filter(e=>e.module==='PERSON'&&e.sev==='high').length}</div><div class="kpi-label">High Error</div></div>
      <div class="kpi-card green"><div class="kpi-val">${fmtNum(state.files['death.txt']?.rows.length||0)}</div><div class="kpi-label">DEATH records</div></div>
    </div>`;

  const colors = ['#0d6e4d','#00b4d8','#f59e0b','#dc2626','#8b5cf6'];
  const labels = ['1 ทะเบียนในเขต/อยู่จริง','2 ทะเบียนในเขต/ตัวไม่อยู่','3 อยู่จริง/ทะเบียนนอกเขต','4 นอกเขตมารับบริการ','5 อื่นๆ'];
  $('typeareaDetail').innerHTML = `
    <div class="typearea-bars">
      ${[1,2,3,4,5].map((t,i) => `
        <div class="ta-bar">
          <div class="ta-bar-val">${fmtNum(ta[t]||0)}</div>
          <div class="ta-bar-inner" style="background:${colors[i]};height:${Math.max(8,pct(ta[t]||0))}px;margin-top:4px"></div>
          <div class="ta-bar-label" style="color:${colors[i]}">${labels[i]}</div>
          <div style="font-size:11px;color:var(--text2)">${pct(ta[t]||0)}%</div>
        </div>`).join('')}
    </div>
    <div style="margin-top:12px">
      ${(ta[1]||0) < total*0.1 ? `<div class="alert alert-warning">⚠️ TYPEAREA 1 (ทะเบียนในเขตและอยู่จริง) น้อยมาก (${pct(ta[1]||0)}%) — ตรวจสอบ address/zone mapping</div>` : ''}
      ${(ta[4]||0) > total*0.8 ? `<div class="alert alert-danger">🚨 TYPEAREA 4 (นอกเขตแต่มารับบริการ) สูงมาก (${pct(ta[4]||0)}%) — ตรวจสอบสถานะประชากรก่อน Upload</div>` : ''}
      ${ta.other > 0 ? `<div class="alert alert-warning">⚠️ TYPEAREA ไม่ถูกต้อง: ${fmtNum(ta.other)} ราย</div>` : ''}
    </div>`;
}

function applyTypeareaEdit() {
  const pf = state.files['person.txt'];
  if (!pf) { alert('ไม่พบ person.txt'); return; }
  const pid = ($('taPid').value || '').trim();
  const from = $('taFrom').value;
  const to = $('taTo').value;
  if (!['1','2','3','4','5'].includes(to)) { alert('TYPEAREA ปลายทางต้องเป็น 1-5'); return; }

  const targets = [];
  pf.rows.forEach((row, i) => {
    const rowPid = getVal('person.txt', row, 'PID');
    const current = getVal('person.txt', row, 'TYPEAREA');
    if (pid && rowPid !== pid) return;
    if (!pid && from && current !== from) return;
    if (pid && from && current !== from) return;
    targets.push({ rowNum: i + 2, pid: rowPid, before: current });
  });

  if (!targets.length) { alert('ไม่พบรายการ PERSON ที่ตรงเงื่อนไข'); return; }
  const msg = pid
    ? `ยืนยันเปลี่ยน TYPEAREA ของ PID ${pid} เป็น ${to}?`
    : `ยืนยันเปลี่ยน TYPEAREA ${from || 'ทุกค่า'} เป็น ${to} จำนวน ${targets.length} ราย?`;
  if (!confirm(msg)) return;

  targets.forEach(t => {
    const before = setCellValue('person.txt', t.rowNum, 'TYPEAREA', to);
    addLog('MANUAL_EDIT', `แก้ TYPEAREA person.txt row ${t.rowNum} PID ${t.pid}`, before, to);
  });
  refreshChecksAfterEdit('person');
  renderAuditLog();
  alert(`แก้ TYPEAREA สำเร็จ ${targets.length} ราย`);
}

// ========== CHRONIC PAGE ==========
function renderChronicPage() {
  const cf = state.files['chronic.txt'];
  const fuFile = state.files['chronicfu.txt'];
  const chronicPids = state._chronicPids || new Set();
  const fuPids = state._fuPids || new Set();
  const noFuPids = state._noFuPids || [];

  // Disease breakdown
  const diseaseMap = {};
  if (cf) {
    cf.rows.forEach(row => {
      const code = getVal('chronic.txt', row, 'CHRONIC');
      const cat = code.startsWith('E1') ? 'DM' : code.startsWith('I1') ? 'HT' : code.startsWith('N18') ? 'CKD' : code.startsWith('J44') ? 'COPD' : code.startsWith('J45') ? 'ASTHMA' : code.startsWith('I6') ? 'STROKE' : code.startsWith('I2') ? 'IHD' : 'อื่นๆ';
      diseaseMap[cat] = (diseaseMap[cat]||0) + 1;
    });
  }

  $('chronicStats').innerHTML = `
    <div class="kpi-grid">
      <div class="kpi-card green"><div class="kpi-val">${fmtNum(chronicPids.size)}</div><div class="kpi-label">CHRONIC ทั้งหมด</div></div>
      ${Object.entries(diseaseMap).map(([cat,n]) => `<div class="kpi-card blue"><div class="kpi-val">${fmtNum(n)}</div><div class="kpi-label">${cat}</div></div>`).join('')}
    </div>`;

  $('chronicFuStats').innerHTML = `
    <div class="kpi-grid">
      <div class="kpi-card green"><div class="kpi-val">${fmtNum(fuFile?.rows.length||0)}</div><div class="kpi-label">CHRONICFU records</div></div>
      <div class="kpi-card green"><div class="kpi-val">${fmtNum(fuPids.size)}</div><div class="kpi-label">ผู้ป่วยที่มี FU</div></div>
      <div class="kpi-card red"><div class="kpi-val red">${fmtNum(noFuPids.length)}</div><div class="kpi-label">ไม่มี FU</div></div>
    </div>`;

  // No FU list
  if (noFuPids.length > 0) {
    const pf = state.files['person.txt'];
    const personMap = {};
    if (pf) pf.rows.forEach(r => { const pid = getVal('person.txt',r,'PID'); if(pid) personMap[pid] = r; });
    
    $('chronicMissingFu').innerHTML = `
      <div class="tbl-wrap" style="max-height:300px;overflow-y:auto">
        <table>
          <thead><tr><th>PID</th><th>CID</th><th>ชื่อ</th><th>โรค (CHRONIC code)</th><th>Action</th></tr></thead>
          <tbody>
            ${noFuPids.slice(0,100).map(pid => {
              const prow = personMap[pid];
              const name = prow ? `${getVal('person.txt',prow,'NAME')} ${getVal('person.txt',prow,'LNAME')}` : '—';
              const cid = prow ? getVal('person.txt',prow,'CID') : '—';
              const diseases = cf ? [...new Set(cf.rows.filter(r=>getVal('chronic.txt',r,'PID')===pid).map(r=>getVal('chronic.txt',r,'CHRONIC')))].join(', ') : '—';
              return `<tr><td class="mono">${escapeHtml(pid)}</td><td class="mono">${escapeHtml(cid)}</td><td>${escapeHtml(name)}</td><td>${escapeHtml(diseases)}</td><td><button class="btn btn-warning btn-sm" onclick="openChronicFuEditor('${escapeHtml(pid)}')">เพิ่ม FU</button></td></tr>`;
            }).join('')}
            ${noFuPids.length > 100 ? `<tr><td colspan="5" style="text-align:center;color:var(--text2);font-style:italic">แสดงเพียง 100 รายการแรก (ทั้งหมด ${fmtNum(noFuPids.length)} ราย)</td></tr>` : ''}
          </tbody>
        </table>
      </div>`;
  } else {
    $('chronicMissingFu').innerHTML = '<div class="alert alert-success">✅ ผู้ป่วย CHRONIC ทุกรายมี CHRONICFU</div>';
  }

  // Candidates
  const candidates = state._candidatePids || new Set();
  if (candidates.size > 0) {
    const info = state._candidateChronicInfo || {};
    $('chronicCandidates').innerHTML = `
      <div class="alert alert-info">พบ <strong>${candidates.size} ราย</strong> ที่มี Diagnosis DM (E10-E14) หรือ HT (I10-I15) ใน DIAGNOSIS_OPD แต่ยังไม่ขึ้นทะเบียน CHRONIC</div>
      <div class="btn-group" style="margin-bottom:10px">
        <button class="btn btn-warning btn-sm" onclick="bulkRegisterChronicCandidates()">ขึ้นทะเบียนทั้งหมดตาม Diag ล่าสุด</button>
      </div>
      <div class="tbl-wrap" style="max-height:200px;overflow-y:auto">
        <table><thead><tr><th>PID</th><th>CID</th><th>CHRONIC</th><th>DATE_DIAG</th><th>Action</th></tr></thead>
        <tbody>${[...candidates].slice(0,100).map(pid=>`<tr><td class="mono">${escapeHtml(pid)}</td><td class="mono">${escapeHtml(info[pid]?.cid || personCidByPid(pid))}</td><td class="mono">${escapeHtml(info[pid]?.chronic || '')}</td><td class="mono">${escapeHtml(info[pid]?.dateServ || '')}</td><td><button class="btn btn-primary btn-sm" onclick="openChronicRegisterEditor('${escapeHtml(pid)}')">ขึ้น CHRONIC</button></td></tr>`).join('')}</tbody></table>
      </div>`;
  } else {
    $('chronicCandidates').innerHTML = '<div class="alert alert-success">✅ ไม่พบผู้ป่วยที่ควรขึ้นทะเบียนเพิ่ม</div>';
  }
}

function openChronicFuEditor(pid='') {
  const cfHeader = ['HOSPCODE','PID','SEQ','DATE_SERV','WEIGHT','HEIGHT','WAIST_CM','SBP','DBP','FOOT','RETINA','PROVIDER','D_UPDATE','CHRONICFUPLACE','CID'];
  const fu = ensureFile('chronicfu.txt', cfHeader);
  const cid = personCidByPid(pid);
  const has = col => colIdx('chronicfu.txt', col) >= 0;
  const rowFor = (label, id, value='', type='text') => `
    <label>${label}</label><input id="${id}" class="mono" type="${type}" value="${escapeHtml(value)}">`;
  const optionalRows = [
    has('CID') ? rowFor('CID', 'fuCID', cid) : '',
    has('WEIGHT') ? rowFor('WEIGHT', 'fuWEIGHT') : '',
    has('HEIGHT') ? rowFor('HEIGHT', 'fuHEIGHT') : '',
    has('WAIST_CM') ? rowFor('WAIST_CM', 'fuWAIST_CM') : '',
    has('SBP') ? rowFor('SBP', 'fuSBP') : '',
    has('DBP') ? rowFor('DBP', 'fuDBP') : '',
    has('FOOT') ? `<label>FOOT</label><select id="fuFOOT"><option value=""></option><option value="1">1 ปกติ</option><option value="2">2 ผิดปกติ</option><option value="3">3 ไม่ตรวจ</option></select>` : '',
    has('RETINA') ? `<label>RETINA</label><select id="fuRETINA"><option value=""></option><option value="1">1 ปกติ</option><option value="2">2 ผิดปกติ</option><option value="3">3 ไม่ตรวจ</option></select>` : '',
    has('SMOKING') ? `<label>SMOKING</label><select id="fuSMOKING"><option value=""></option><option value="1">1 ไม่สูบ</option><option value="2">2 สูบ</option><option value="3">3 เลิกสูบ</option><option value="9">9 ไม่ทราบ</option></select>` : '',
    has('ALCOHOL') ? `<label>ALCOHOL</label><select id="fuALCOHOL"><option value=""></option><option value="1">1 ไม่ดื่ม</option><option value="2">2 ดื่ม</option><option value="3">3 เลิกดื่ม</option><option value="9">9 ไม่ทราบ</option></select>` : ''
  ].join('');
  const body = `
    <div class="alert alert-warning">เพิ่ม CHRONICFU 1 record ตามโครงสร้าง column ของไฟล์เดิม กรุณาใส่ SEQ ของ visit ที่ติดตามจริงจาก SERVICE และวันที่ DATE_SERV รูปแบบ YYYYMMDD</div>
    <div class="btn-group" style="margin-bottom:12px">
      <button class="btn btn-outline btn-sm" onclick="fillFuFromLatest()">ดึงค่าล่าสุดของ PID นี้</button>
      <button class="btn btn-outline btn-sm" onclick="fillFuFromSeq()">ดึงจาก SERVICE ตาม SEQ</button>
      <button class="btn btn-success btn-sm" onclick="fillFuNormalDefaults()">เติมค่าปกติ</button>
    </div>
    <div class="edit-grid">
      ${rowFor('HOSPCODE', 'fuHOSPCODE', state.hcode)}
      ${rowFor('PID', 'fuPID', pid)}
      ${rowFor('SEQ', 'fuSEQ')}
      ${rowFor('DATE_SERV', 'fuDATE_SERV', ymdToday())}
      ${optionalRows}
      ${has('PROVIDER') ? rowFor('PROVIDER', 'fuPROVIDER') : ''}
      ${has('D_UPDATE') ? rowFor('D_UPDATE', 'fuDUPDATE', ymdToday() + '000000') : ''}
      ${has('CHRONICFUPLACE') ? rowFor('CHRONICFUPLACE', 'fuCHRONICFUPLACE', state.hcode) : ''}
    </div>
    <div class="edit-help">ถ้าไม่มีการติดตามจริง ไม่ควรสร้างข้อมูลแทนผลบริการจริง ให้กลับไปบันทึก visit/FU ในระบบต้นทางแล้ว export 43 แฟ้มใหม่</div>`;
  openModal('เพิ่ม CHRONICFU', body, `<button class="btn btn-primary btn-sm" onclick="saveChronicFuRecord()">เพิ่ม record และตรวจซ้ำ</button>`);
  fillFuNormalDefaults();
  if (latestRowByPid('chronicfu.txt', pid)) fillFuFromLatest();
}

function fuInput(id) {
  const el = $(id);
  return el ? el.value.trim() : '';
}

function saveChronicFuRecord() {
  const pid = fuInput('fuPID');
  const seq = fuInput('fuSEQ');
  const dateServ = fuInput('fuDATE_SERV');
  if (!pid) { alert('กรุณาระบุ PID'); return; }
  if (!seq) { alert('กรุณาระบุ SEQ ของ visit ที่ติดตาม'); return; }
  if (!isValidDate8(dateServ)) { alert('DATE_SERV ต้องเป็น YYYYMMDD'); return; }
  const values = {
    HOSPCODE: fuInput('fuHOSPCODE') || state.hcode,
    PID: pid,
    CID: fuInput('fuCID') || personCidByPid(pid),
    SEQ: seq,
    DATE_SERV: dateServ,
    WEIGHT: fuInput('fuWEIGHT'),
    HEIGHT: fuInput('fuHEIGHT'),
    WAIST_CM: fuInput('fuWAIST_CM'),
    SBP: fuInput('fuSBP'),
    DBP: fuInput('fuDBP'),
    FOOT: fuInput('fuFOOT'),
    RETINA: fuInput('fuRETINA'),
    SMOKING: fuInput('fuSMOKING'),
    ALCOHOL: fuInput('fuALCOHOL'),
    PROVIDER: fuInput('fuPROVIDER'),
    D_UPDATE: fuInput('fuDUPDATE') || ymdToday() + '000000',
    CHRONICFUPLACE: fuInput('fuCHRONICFUPLACE') || state.hcode
  };
  const rowNum = appendRow('chronicfu.txt', values);
  addLog('MANUAL_EDIT', `เพิ่ม CHRONICFU row ${rowNum} PID ${pid} SEQ ${seq}`, '', JSON.stringify(values));
  closeModal();
  refreshChecksAfterEdit('chronic');
  renderAuditLog();
}

function dmCandidateMap() {
  const out = new Map();
  const add = (pid, source, code, date='', cid='') => {
    if (!pid || !isDmCode(code)) return;
    const cur = out.get(pid);
    if (!cur || date > cur.date) out.set(pid, { pid, source, code, date, cid: cid || personCidByPid(pid) });
  };
  const chronic = state.files['chronic.txt'];
  if (chronic) chronic.rows.forEach(r => add(getVal('chronic.txt', r, 'PID'), 'CHRONIC', getVal('chronic.txt', r, 'CHRONIC'), getVal('chronic.txt', r, 'DATE_DIAG'), getVal('chronic.txt', r, 'CID')));
  const dopd = state.files['diagnosis_opd.txt'];
  if (dopd) dopd.rows.forEach(r => add(getVal('diagnosis_opd.txt', r, 'PID'), 'DIAGNOSIS_OPD', getVal('diagnosis_opd.txt', r, 'DIAGCODE'), getVal('diagnosis_opd.txt', r, 'DATE_SERV'), getVal('diagnosis_opd.txt', r, 'CID')));
  const dipd = state.files['diagnosis_ipd.txt'];
  if (dipd) dipd.rows.forEach(r => add(getVal('diagnosis_ipd.txt', r, 'PID'), 'DIAGNOSIS_IPD', getVal('diagnosis_ipd.txt', r, 'DIAGCODE'), getVal('diagnosis_ipd.txt', r, 'DATETIME_ADMIT').slice(0,8), getVal('diagnosis_ipd.txt', r, 'CID')));
  return out;
}

function personEligibility(pid) {
  const pf = state.files['person.txt'];
  if (!pf) return { ok:false, reason:'ไม่มี PERSON' };
  const rowNum = findRowNumByPid('person.txt', pid);
  if (!rowNum) return { ok:false, reason:'PID ไม่พบใน PERSON' };
  const row = pf.rows[rowNum - 2];
  const typearea = getVal('person.txt', row, 'TYPEAREA');
  const discharge = getVal('person.txt', row, 'DISCHARGE');
  const nation = getVal('person.txt', row, 'NATION');
  const reasons = [];
  if (!['1','3'].includes(typearea)) reasons.push(`TYPEAREA=${typearea || 'ว่าง'} ไม่ใช่ 1/3`);
  if (discharge !== '9') reasons.push(`DISCHARGE=${discharge || 'ว่าง'} ไม่ใช่ 9`);
  if (nation !== '099') reasons.push(`NATION=${nation || 'ว่าง'} ไม่ใช่ 099`);
  return { ok: reasons.length === 0, reason: reasons.join(', '), row, rowNum, typearea, discharge, nation, cid:getVal('person.txt', row, 'CID') };
}

function retinaRowsByPid(pid, fy) {
  const cf = state.files['chronicfu.txt'];
  if (!cf) return [];
  const { start, end } = fiscalRange(fy);
  return cf.rows
    .map((row, i) => ({ row, rowNum:i+2, date:getVal('chronicfu.txt', row, 'DATE_SERV'), retina:getVal('chronicfu.txt', row, 'RETINA'), seq:getVal('chronicfu.txt', row, 'SEQ') }))
    .filter(x => getVal('chronicfu.txt', x.row, 'PID') === String(pid) && x.date >= start && x.date <= end)
    .sort((a,b) => b.date.localeCompare(a.date));
}

function buildRetinaAuditRows() {
  const fy = Number($('retinaFy')?.value || currentThaiFiscalYear());
  const dm = dmCandidateMap();
  const rows = [];
  dm.forEach((info, pid) => {
    const person = personEligibility(pid);
    const fus = retinaRowsByPid(pid, fy);
    const retinaFu = fus.find(x => ['1','2','3','4'].includes(x.retina));
    const abnormalFu = fus.find(x => ['1','2'].includes(x.retina));
    let status = 'ไม่นับ';
    let reason = '';
    if (!person.ok) reason = person.reason;
    else if (!fus.length) reason = 'ไม่มี CHRONICFU ในปีงบประมาณ';
    else if (!retinaFu) reason = `มี CHRONICFU แต่ RETINA ไม่ใช่ 1-4 (${[...new Set(fus.map(x=>x.retina || 'ว่าง'))].join(', ')})`;
    else status = 'นับตรวจตา';
    rows.push({
      pid, cid: person.cid || info.cid || '', name: person.row ? `${getVal('person.txt', person.row, 'NAME')} ${getVal('person.txt', person.row, 'LNAME')}` : '',
      typearea: person.typearea || '', discharge: person.discharge || '', nation: person.nation || '',
      chronic: info.code, source: info.source, dateDiag: info.date, fuCount: fus.length,
      latestFuRow: fus[0]?.rowNum || '', latestSeq: fus[0]?.seq || '', latestDate: fus[0]?.date || '', retina: fus[0]?.retina || '',
      counted: Boolean(person.ok && retinaFu), abnormal: Boolean(person.ok && abnormalFu), status, reason
    });
  });
  return { fy, rows };
}

function renderRetinaAudit() {
  if (!$('retinaAudit')) return;
  if (!$('retinaFy').value) $('retinaFy').value = currentThaiFiscalYear();
  const { fy, rows } = buildRetinaAuditRows();
  state._retinaAuditRows = rows;
  const eligible = rows.filter(r => !r.reason.startsWith('TYPEAREA') && !r.reason.includes('DISCHARGE') && !r.reason.includes('NATION') && r.typearea && ['1','3'].includes(r.typearea) && r.discharge === '9' && r.nation === '099');
  const counted = rows.filter(r => r.counted);
  const missingFu = rows.filter(r => r.reason === 'ไม่มี CHRONICFU ในปีงบประมาณ');
  const badRetina = rows.filter(r => r.reason.startsWith('มี CHRONICFU แต่ RETINA'));
  const notPerson = rows.filter(r => !r.typearea || !['1','3'].includes(r.typearea) || r.discharge !== '9' || r.nation !== '099');
  const pct = eligible.length ? Math.round(counted.length / eligible.length * 1000) / 10 : 0;
  const list = rows.filter(r => !r.counted).slice(0,120);
  $('retinaAudit').innerHTML = `
    <div class="alert alert-info">ตามเอกสาร HDC ปี 2567: ตรวจตาเบาหวานนับจาก CHRONICFU.RETINA = 1-4 ในปีงบประมาณ และกลุ่มเป้าหมายต้อง TYPEAREA 1/3, DISCHARGE 9, NATION 099</div>
    <div class="kpi-grid">
      <div class="kpi-card blue"><div class="kpi-val">${fmtNum(eligible.length)}</div><div class="kpi-label">DM เข้าเกณฑ์พื้นที่</div></div>
      <div class="kpi-card green"><div class="kpi-val">${fmtNum(counted.length)}</div><div class="kpi-label">นับตรวจตา RETINA 1-4</div></div>
      <div class="kpi-card yellow"><div class="kpi-val yellow">${pct}%</div><div class="kpi-label">Coverage FY ${fy}</div></div>
      <div class="kpi-card red"><div class="kpi-val red">${fmtNum(missingFu.length)}</div><div class="kpi-label">ไม่มี CHRONICFU</div></div>
      <div class="kpi-card red"><div class="kpi-val red">${fmtNum(badRetina.length)}</div><div class="kpi-label">RETINA ไม่ใช่ 1-4</div></div>
      <div class="kpi-card yellow"><div class="kpi-val yellow">${fmtNum(notPerson.length)}</div><div class="kpi-label">ติด TYPEAREA/DISCHARGE/NATION</div></div>
    </div>
    <div class="tbl-wrap" style="max-height:320px;overflow-y:auto">
      <table>
        <thead><tr><th>PID</th><th>CID</th><th>ชื่อ</th><th>TYPEAREA</th><th>DM code</th><th>FU ล่าสุด</th><th>RETINA</th><th>สาเหตุที่ไม่ขึ้น</th><th>Action</th></tr></thead>
        <tbody>${list.map(r => `
          <tr>
            <td class="mono">${escapeHtml(r.pid)}</td><td class="mono">${escapeHtml(r.cid)}</td><td>${escapeHtml(r.name)}</td>
            <td class="mono">${escapeHtml(r.typearea)}</td><td class="mono">${escapeHtml(r.chronic)}</td>
            <td class="mono">${escapeHtml(r.latestDate || '-')} ${r.latestSeq ? '('+escapeHtml(r.latestSeq)+')' : ''}</td>
            <td class="mono">${escapeHtml(r.retina || '-')}</td><td>${escapeHtml(r.reason)}</td>
            <td><div class="action-stack">
              ${r.latestFuRow ? `<button class="btn btn-primary btn-sm" onclick="openRetinaEditor('${escapeHtml(r.pid)}', ${r.latestFuRow})">แก้ RETINA</button>` : `<button class="btn btn-warning btn-sm" onclick="openRetinaEditor('${escapeHtml(r.pid)}', 0)">เพิ่ม FU/RETINA</button>`}
              ${(!['1','3'].includes(r.typearea) && r.typearea) ? `<button class="btn btn-outline btn-sm" onclick="quickSetTypearea('${escapeHtml(r.pid)}','1')">TYPEAREA=1</button>` : ''}
            </div></td>
          </tr>`).join('')}
        </tbody>
      </table>
    </div>`;
}

function quickSetTypearea(pid, value) {
  const rowNum = findRowNumByPid('person.txt', pid);
  if (!rowNum) return;
  const before = setCellValue('person.txt', rowNum, 'TYPEAREA', value);
  addLog('MANUAL_EDIT', `แก้ TYPEAREA จาก Retina audit PID ${pid}`, before, value);
  refreshChecksAfterEdit('chronic');
  renderAuditLog();
}

function openRetinaEditor(pid, rowNum=0) {
  if (rowNum) {
    const row = state.files['chronicfu.txt'].rows[rowNum - 2];
    const current = getVal('chronicfu.txt', row, 'RETINA');
    const body = `
      <div class="alert alert-info">แก้ค่า RETINA ใน CHRONICFU row ${rowNum} ให้เป็นค่าที่ HDC นับได้ ถ้ามีการตรวจจริง</div>
      <div class="edit-grid">
        <label>PID / SEQ</label><div class="mono">${escapeHtml(pid)} / ${escapeHtml(getVal('chronicfu.txt', row, 'SEQ'))}</div>
        <label>DATE_SERV</label><div class="mono">${escapeHtml(getVal('chronicfu.txt', row, 'DATE_SERV'))}</div>
        <label>RETINA</label><select id="retinaValue"><option value="1">1 ปกติ</option><option value="2">2 ผิดปกติ</option><option value="3">3 ตรวจไม่ได้/ไม่ทราบผล</option><option value="4">4 ได้รับการตรวจ</option><option value="9">9 ไม่ตรวจ</option></select>
      </div>`;
    openModal('แก้ RETINA', body, `<button class="btn btn-primary btn-sm" onclick="saveRetinaValue(${rowNum})">บันทึก RETINA</button>`);
    $('retinaValue').value = ['1','2','3','4','9'].includes(current) ? current : '1';
    return;
  }
  openChronicFuEditor(pid);
  setFuField('fuRETINA', '1');
}

function saveRetinaValue(rowNum) {
  const val = $('retinaValue').value;
  const before = setCellValue('chronicfu.txt', rowNum, 'RETINA', val);
  addLog('MANUAL_EDIT', `แก้ CHRONICFU.RETINA row ${rowNum}`, before, val);
  closeModal();
  refreshChecksAfterEdit('chronic');
  renderAuditLog();
}

function exportRetinaAuditExcel() {
  const rows = state._retinaAuditRows || buildRetinaAuditRows().rows;
  if (!rows.length) { alert('ไม่มีข้อมูล Retina audit'); return; }
  const data = rows.map((r,i) => ({
    '#': i+1, PID:r.pid, CID:r.cid, NAME:r.name, TYPEAREA:r.typearea, DISCHARGE:r.discharge, NATION:r.nation,
    DM_CODE:r.chronic, SOURCE:r.source, DATE_DIAG:r.dateDiag, FU_COUNT:r.fuCount, LATEST_FU_ROW:r.latestFuRow,
    LATEST_SEQ:r.latestSeq, LATEST_DATE:r.latestDate, RETINA:r.retina, COUNTED:r.counted?'Y':'N', ABNORMAL:r.abnormal?'Y':'N', REASON:r.reason
  }));
  const ws = XLSX.utils.json_to_sheet(data);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Retina Audit');
  XLSX.writeFile(wb, `RetinaAudit_${state.hcode}_${$('retinaFy').value || currentThaiFiscalYear()}.xlsx`);
  addLog('EXPORT', `Export Retina Audit (${rows.length} ราย)`);
  renderAuditLog();
}

function setFuField(id, value) {
  const el = $(id);
  if (el && value !== undefined && value !== null && value !== '') el.value = value;
}

function fillFuFromLatest() {
  const pid = fuInput('fuPID');
  const latest = latestRowByPid('chronicfu.txt', pid);
  if (!latest) { alert('ไม่พบ CHRONICFU เดิมของ PID นี้'); return; }
  ['WEIGHT','HEIGHT','WAIST_CM','SBP','DBP','FOOT','RETINA','SMOKING','ALCOHOL','PROVIDER','CHRONICFUPLACE'].forEach(f => {
    setFuField('fu' + f, getVal('chronicfu.txt', latest, f));
  });
}

function fillFuNormalDefaults() {
  setFuField('fuSBP', '120');
  setFuField('fuDBP', '80');
  setFuField('fuFOOT', '1');
  setFuField('fuRETINA', '1');
  setFuField('fuSMOKING', '1');
  setFuField('fuALCOHOL', '1');
  setFuField('fuCHRONICFUPLACE', state.hcode);
}

function fillFuFromSeq() {
  const seq = fuInput('fuSEQ');
  const svc = findServiceBySeq(seq);
  if (!svc) { alert('ไม่พบ SEQ นี้ใน service.txt'); return; }
  setFuField('fuPID', getVal('service.txt', svc, 'PID'));
  setFuField('fuDATE_SERV', getVal('service.txt', svc, 'DATE_SERV'));
  setFuField('fuCID', getVal('service.txt', svc, 'CID'));
  setFuField('fuSBP', getVal('service.txt', svc, 'SBP'));
  setFuField('fuDBP', getVal('service.txt', svc, 'DBP'));
}

function openChronicRegisterEditor(pid='') {
  ensureFile('chronic.txt', ['HOSPCODE','PID','DATE_DIAG','CHRONIC','HOSP_DX','HOSP_RX','DATE_DISCH','TYPEDISCH','D_UPDATE','CID']);
  const info = (state._candidateChronicInfo || {})[pid] || {};
  const cid = info.cid || personCidByPid(pid);
  const body = `
    <div class="alert alert-warning">ขึ้นทะเบียน CHRONIC ควรทำเมื่อยืนยันแล้วว่าเป็นโรคเรื้อรังจริงตามเวชระเบียน ไม่ใช่เพียง diagnosis ชั่วคราว</div>
    <div class="edit-grid">
      <label>HOSPCODE</label><input id="chHOSPCODE" class="mono" value="${escapeHtml(state.hcode)}">
      <label>PID</label><input id="chPID" class="mono" value="${escapeHtml(pid)}">
      <label>CID</label><input id="chCID" class="mono" value="${escapeHtml(cid)}">
      <label>DATE_DIAG</label><input id="chDATE_DIAG" class="mono" value="${escapeHtml(info.dateServ || ymdToday())}">
      <label>CHRONIC</label><input id="chCHRONIC" class="mono" value="${escapeHtml(info.chronic || '')}">
      <label>HOSP_DX</label><input id="chHOSP_DX" class="mono" value="${escapeHtml(state.hcode)}">
      <label>HOSP_RX</label><input id="chHOSP_RX" class="mono" value="${escapeHtml(state.hcode)}">
      <label>DATE_DISCH</label><input id="chDATE_DISCH" class="mono" value="">
      <label>TYPEDISCH</label><input id="chTYPEDISCH" class="mono" value="">
      <label>D_UPDATE</label><input id="chDUPDATE" class="mono" value="${ymdToday()}000000">
    </div>`;
  openModal('ขึ้นทะเบียน CHRONIC', body, `<button class="btn btn-primary btn-sm" onclick="saveChronicRegister()">เพิ่ม CHRONIC และตรวจซ้ำ</button>`);
}

function chInput(id) {
  const el = $(id);
  return el ? el.value.trim() : '';
}

function chronicValuesFromDialog() {
  return {
    HOSPCODE: chInput('chHOSPCODE') || state.hcode,
    PID: chInput('chPID'),
    DATE_DIAG: chInput('chDATE_DIAG'),
    CHRONIC: chInput('chCHRONIC').toUpperCase(),
    HOSP_DX: chInput('chHOSP_DX') || state.hcode,
    HOSP_RX: chInput('chHOSP_RX') || state.hcode,
    DATE_DISCH: chInput('chDATE_DISCH'),
    TYPEDISCH: chInput('chTYPEDISCH'),
    D_UPDATE: chInput('chDUPDATE') || ymdToday() + '000000',
    CID: chInput('chCID') || personCidByPid(chInput('chPID'))
  };
}

function saveChronicRegister() {
  const values = chronicValuesFromDialog();
  if (!values.PID) { alert('กรุณาระบุ PID'); return; }
  if (!isValidDate8(values.DATE_DIAG)) { alert('DATE_DIAG ต้องเป็น YYYYMMDD'); return; }
  if (!isICD10(values.CHRONIC)) { alert('CHRONIC ต้องเป็น ICD-10 เช่น E119 หรือ I10'); return; }
  const exists = state.files['chronic.txt']?.rows.some(r => getVal('chronic.txt', r, 'PID') === values.PID && getVal('chronic.txt', r, 'CHRONIC') === values.CHRONIC);
  if (exists) { alert('มี PID + CHRONIC นี้อยู่แล้ว'); return; }
  const rowNum = appendRow('chronic.txt', values);
  addLog('MANUAL_EDIT', `เพิ่ม CHRONIC row ${rowNum} PID ${values.PID} ${values.CHRONIC}`, '', JSON.stringify(values));
  closeModal();
  refreshChecksAfterEdit('chronic');
  renderAuditLog();
}

function bulkRegisterChronicCandidates() {
  const info = state._candidateChronicInfo || {};
  const pids = [...(state._candidatePids || new Set())].filter(pid => info[pid]?.chronic);
  if (!pids.length) { alert('ไม่พบ candidate ที่มีรหัส CHRONIC ชัดเจน'); return; }
  if (!confirm(`ยืนยันเพิ่ม CHRONIC จาก diagnosis ล่าสุด จำนวน ${pids.length} ราย?\\nควรใช้เมื่อยืนยันทะเบียนโรคจริงแล้ว`)) return;
  ensureFile('chronic.txt', ['HOSPCODE','PID','DATE_DIAG','CHRONIC','HOSP_DX','HOSP_RX','DATE_DISCH','TYPEDISCH','D_UPDATE','CID']);
  let added = 0;
  pids.forEach(pid => {
    const item = info[pid];
    const exists = state.files['chronic.txt'].rows.some(r => getVal('chronic.txt', r, 'PID') === pid && getVal('chronic.txt', r, 'CHRONIC') === item.chronic);
    if (exists) return;
    appendRow('chronic.txt', {
      HOSPCODE: state.hcode, PID: pid, DATE_DIAG: item.dateServ || ymdToday(), CHRONIC: item.chronic,
      HOSP_DX: state.hcode, HOSP_RX: state.hcode, DATE_DISCH: '', TYPEDISCH: '',
      D_UPDATE: ymdToday() + '000000', CID: item.cid || personCidByPid(pid)
    });
    added++;
  });
  addLog('MANUAL_EDIT', `เพิ่ม CHRONIC candidate แบบชุด ${added} ราย`, '', '');
  refreshChecksAfterEdit('chronic');
  renderAuditLog();
  alert(`เพิ่ม CHRONIC สำเร็จ ${added} ราย`);
}

// ========== LAB PAGE ==========
function renderLabPage() {
  const lf = state.files['labfu.txt'];
  const unmapped = state._unmappedLabCodes || new Map();
  const outliers = state._labOutliers || [];

  $('labStats').innerHTML = `
    <div class="kpi-grid">
      <div class="kpi-card blue"><div class="kpi-val">${fmtNum(lf?.rows.length||0)}</div><div class="kpi-label">LABFU records</div></div>
      <div class="kpi-card green"><div class="kpi-val">${labMapping.length}</div><div class="kpi-label">Mapped Lab Codes</div></div>
      <div class="kpi-card red"><div class="kpi-val red">${unmapped.size}</div><div class="kpi-label">ยังไม่ map</div></div>
      <div class="kpi-card yellow"><div class="kpi-val yellow">${outliers.length}</div><div class="kpi-label">Outlier results</div></div>
    </div>`;

  $('labOutliers').innerHTML = outliers.length > 0 ?
    `<div class="tbl-wrap" style="max-height:200px;overflow-y:auto">
      <table><thead><tr><th>Row</th><th>PID</th><th>Lab</th><th>Result</th><th>Category</th></tr></thead>
      <tbody>${outliers.slice(0,50).map(o=>`<tr><td class="mono">${o.ri}</td><td class="mono">${o.pid}</td><td class="mono">${o.labtest}</td><td class="mono" style="color:var(--danger);font-weight:700">${o.labresult}</td><td>${o.cat}</td></tr>`).join('')}</tbody>
      </table></div>` :
    '<div class="alert alert-success">✅ ไม่พบค่า Lab Outlier</div>';

  if (unmapped.size > 0) {
    const rows = [...unmapped.entries()].sort((a,b)=>b[1]-a[1]).map(([code,cnt]) =>
      `<tr><td class="mono">${code}</td><td>${fmtNum(cnt)}</td>
       <td><button class="btn btn-primary btn-sm" onclick="addMappingRowWithCode('${code}')">➕ เพิ่ม Mapping</button></td></tr>`
    ).join('');
    $('labUnmapped').innerHTML = `<div class="tbl-wrap"><table><thead><tr><th>Lab Code</th><th>จำนวนครั้ง</th><th>Action</th></tr></thead><tbody>${rows}</tbody></table></div>`;
  } else {
    $('labUnmapped').innerHTML = '<div class="alert alert-success">✅ Lab Code ทุกรายการพบใน Mapping Table</div>';
  }
}

// ========== MAPPING TABLE ==========
function renderMappingTable() {
  $('mappingTableBody').innerHTML = labMapping.map((m, i) => `
    <tr>
      <td class="mono"><input type="text" value="${m.local}" onchange="labMapping[${i}].local=this.value" style="border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-family:'IBM Plex Mono',monospace;font-size:12px;width:90px"></td>
      <td><input type="text" value="${m.localName}" onchange="labMapping[${i}].localName=this.value" style="border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:12px;width:160px"></td>
      <td class="mono"><input type="text" value="${m.hdc}" onchange="labMapping[${i}].hdc=this.value" style="border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-family:'IBM Plex Mono',monospace;font-size:12px;width:90px"></td>
      <td>
        <select onchange="labMapping[${i}].category=this.value" style="border:1px solid var(--border);padding:3px;border-radius:4px;font-size:12px">
          ${['HbA1c','FBS','LDL','Creatinine','eGFR','Microalbumin','Lipid','Urine','CBC','อื่นๆ'].map(c=>`<option ${m.category===c?'selected':''}>${c}</option>`).join('')}
        </select>
      </td>
      <td><input type="text" value="${m.unit}" onchange="labMapping[${i}].unit=this.value" style="border:1px solid var(--border);padding:3px 6px;border-radius:4px;font-size:12px;width:80px"></td>
      <td><span class="sev ${m.active?'sev-info':'sev-critical'}">${m.active?'Active':'Inactive'}</span></td>
      <td><button class="btn btn-danger btn-sm" onclick="removeMappingRow(${i})">✕</button></td>
    </tr>`).join('');
}

function addMappingRow() {
  labMapping.push({ local:'', localName:'', hdc:'', category:'อื่นๆ', unit:'', active:true });
  renderMappingTable();
  addLog('MANUAL_EDIT', 'เพิ่ม Lab Mapping row ใหม่');
}

function addMappingRowWithCode(code) {
  showPage('mapping');
  labMapping.push({ local:code, localName:'', hdc:'', category:'อื่นๆ', unit:'', active:true });
  renderMappingTable();
  addLog('MANUAL_EDIT', `เพิ่ม Lab Mapping สำหรับ code: ${code}`);
}

function removeMappingRow(i) {
  const removed = labMapping[i];
  labMapping.splice(i, 1);
  renderMappingTable();
  addLog('MANUAL_EDIT', `ลบ Lab Mapping: ${removed.local} (${removed.localName})`);
}

function exportMapping() {
  const ws = XLSX.utils.json_to_sheet(labMapping.map(m => ({
    'Local Code': m.local, 'Local Name': m.localName, 'HDC Code': m.hdc,
    'Category': m.category, 'Unit': m.unit, 'Active': m.active ? 'Y' : 'N'
  })));
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Lab Mapping');
  XLSX.writeFile(wb, `LabMapping_${state.hcode}.xlsx`);
  addLog('EXPORT', 'Export Lab Mapping Excel');
}

// ========== AUDIT LOG ==========
function renderAuditLog() {
  const filter = $('logFilter').value;
  const filtered = filter ? state.auditLog.filter(l => l.action === filter) : state.auditLog;
  $('logCount').textContent = fmtNum(filtered.length) + ' รายการ';
  const actionColors = { AUTO_FIX: 'var(--success)', MANUAL_EDIT: 'var(--accent)', IGNORE: 'var(--text2)', RESTORE: 'var(--warning)', UPLOAD: 'var(--primary)', CHECK: 'var(--primary)', EXPORT: '#8b5cf6' };
  $('auditLogList').innerHTML = filtered.length ? [...filtered].reverse().map(l => `
    <div class="log-entry">
      <span class="log-time">${l.time}</span>
      <span class="log-action" style="color:${actionColors[l.action]||'var(--text)'}">[${l.action}]</span>
      <span class="log-detail">${l.detail}</span>
      ${l.before ? `<span class="mono" style="color:var(--danger);font-size:11px">${l.before}</span><span style="color:var(--text2)">→</span>` : ''}
      ${l.after ? `<span class="mono" style="color:var(--success);font-size:11px">${l.after}</span>` : ''}
    </div>`).join('') : '<div class="empty-state" style="padding:30px"><div class="es-icon">📋</div><div>ยังไม่มีการดำเนินการ</div></div>';
}

// ========== EXPORT ==========
async function exportFixedZip() {
  if (!Object.keys(state.files).length) { alert('ไม่มีข้อมูล'); return; }
  loading(true, 'กำลัง Export ZIP...', '');
  const zip = new JSZip();
  const folder = zip.folder(`F43_${state.hcode}_FIXED`);
  Object.entries(state.files).forEach(([name, f]) => {
    touchFile(name);
    folder.file(name, f.rawText, { binary: false });
  });
  const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a'); a.href = url;
  a.download = `F43_${state.hcode}_FIXED_${new Date().toISOString().slice(0,10).replace(/-/g,'')}.zip`;
  a.click();
  URL.revokeObjectURL(url);
  addLog('EXPORT', 'Export ZIP 43 แฟ้มที่แก้ไขแล้ว');
  loading(false);
  renderAuditLog();
}

function exportErrorExcel() {
  if (!state.errors.length) { alert('ไม่มี Error'); return; }
  const data = state.errors.map((e,i) => ({
    '#': i+1, 'Severity': e.sev, 'Module': e.module, 'File': e.file, 'Row': e.rowNum,
    'PID': e.pid, 'CID': e.cid, 'SEQ': e.seq, 'Field': e.field,
    'ค่าปัจจุบัน': e.curVal, 'ค่าที่แนะนำ': e.sugVal, 'คำอธิบาย': e.desc, 'Action': e.action
  }));
  const ws = XLSX.utils.json_to_sheet(data);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Error Report');
  XLSX.writeFile(wb, `HDC_Error_${state.hcode}_${new Date().toISOString().slice(0,10).replace(/-/g,'')}.xlsx`);
  addLog('EXPORT', `Export Error Report Excel (${state.errors.length} รายการ)`);
  renderAuditLog();
}

function exportAuditExcel() {
  if (!state.auditLog.length) { alert('ไม่มีข้อมูล Audit Log'); return; }
  const data = state.auditLog.map((l,i) => ({
    '#': i+1, 'เวลา': l.time, 'Action': l.action, 'รายละเอียด': l.detail, 'ก่อน': l.before, 'หลัง': l.after
  }));
  const ws = XLSX.utils.json_to_sheet(data);
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, 'Audit Log');
  XLSX.writeFile(wb, `AuditLog_${state.hcode}_${new Date().toISOString().slice(0,10).replace(/-/g,'')}.xlsx`);
}

function resetApp() {
  if (!confirm('รีเซ็ตข้อมูลทั้งหมด?')) return;
  state = { files:{}, errors:[], auditLog:[], fixes:{}, originals:{}, hcode:'11422', checked:false, charts:{} };
  $('fileSummarySection').style.display = 'none';
  $('progressWrap').style.display = 'none';
  $('topHcode').textContent = 'HCODE: —';
  $('fileInput').value = '';
  showPage('upload');
}

// ========== INIT ==========
renderMappingTable();
renderAuditLog();
</script>
</body>
</html>


Youez - 2016 - github.com/yon3zu
LinuXploit