Files
justice/www/html/testcase/index.php
T
2026-07-04 14:02:12 +00:00

429 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Justice — Test Case / Bug List tracker (QA verification + admin + categories + dev edit)
$DATA_DIR = __DIR__ . '/data';
$BUGS = $DATA_DIR . '/bugs.json';
$VERIFY = $DATA_DIR . '/verify.json';
$CATS = $DATA_DIR . '/cats.json';
$UPLOADS = __DIR__ . '/uploads';
$ADMIN_PASS = 'justice2026'; // รหัสโหมดแอดมิน
function tc_load($f){ return is_file($f) ? (json_decode(file_get_contents($f), true) ?: []) : []; }
function tc_save($f,$d){ file_put_contents($f, json_encode($d, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), LOCK_EX); }
// หมวดเริ่มต้น (แก้ไข/เพิ่ม/ลบได้ผ่านโหมดแอดมิน)
$DEFAULT_CATS = ['LobbyA','LobbyB','ระบบ disconnect','Minigame 1','Minigame 2','Minigame 3','Minigame 4','Minigame 5','Minigame 6','Minigame 7','อื่นๆ'];
function tc_cats($CATS,$DEFAULT_CATS){
$c = tc_load($CATS);
if (!is_array($c) || !count($c)) { $c = $DEFAULT_CATS; tc_save($CATS,$c); }
return array_values(array_filter(array_map('strval',$c), function($x){ return trim($x)!==''; }));
}
$is_admin = (($_POST['pass'] ?? '') === $ADMIN_PASS);
// ---------- API ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['api'])) {
header('Content-Type: application/json; charset=utf-8');
$api = $_POST['api'];
// QA save (เดิม) — ใช้รหัสแอดมิน
if ($api === 'save') {
if (!$is_admin) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'รหัสแอดมินไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$verify = tc_load($VERIFY);
$rec = $verify[$id] ?? ['result'=>'none','comment'=>'','images'=>[]];
if (!isset($rec['images']) || !is_array($rec['images'])) $rec['images'] = [];
$res = $_POST['result'] ?? '';
if (in_array($res, ['pass','fail','none'], true)) $rec['result'] = $res;
if (isset($_POST['comment'])) $rec['comment'] = mb_substr(trim((string)$_POST['comment']), 0, 2000);
if (($_POST['removeImage'] ?? '') !== '') {
$rm = basename((string)$_POST['removeImage']);
$rec['images'] = array_values(array_filter($rec['images'], function($x) use ($rm){ return $x !== $rm; }));
@unlink($UPLOADS . '/' . $rm);
}
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
$ext = strtolower(pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION));
if (in_array($ext, ['jpg','jpeg','png','gif','webp'], true) && $_FILES['image']['size'] < 8*1024*1024) {
if (!is_dir($UPLOADS)) @mkdir($UPLOADS, 0775, true);
$fn = $id . '_' . time() . '_' . bin2hex(random_bytes(3)) . '.' . $ext;
if (move_uploaded_file($_FILES['image']['tmp_name'], $UPLOADS . '/' . $fn)) $rec['images'][] = $fn;
}
}
$rec['at'] = date('Y-m-d H:i');
$verify[$id] = $rec;
tc_save($VERIFY, $verify);
echo json_encode(['ok'=>true,'rec'=>$rec], JSON_UNESCAPED_UNICODE); exit;
}
// ทุก API ด้านล่างต้องเป็นแอดมิน
if (!$is_admin) { http_response_code(403); echo json_encode(['ok'=>false,'error'=>'ต้องใส่รหัสแอดมิน'], JSON_UNESCAPED_UNICODE); exit; }
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
// เพิ่ม/แก้ไขบั๊ก (Dev) — id ว่าง = เพิ่มใหม่
if ($api === 'bugsave') {
$bugs = tc_load($BUGS);
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
$row = [
'name' => mb_substr(trim((string)($_POST['name'] ?? '')), 0, 300),
'detail' => mb_substr(trim((string)($_POST['detail'] ?? '')), 0, 4000),
'prio' => in_array(($_POST['prio'] ?? ''), ['วิกฤต','สูง','กลาง','ต่ำ',''], true) ? ($_POST['prio'] ?? '') : '',
'status' => in_array(($_POST['status'] ?? ''), ['เปิด','แก้ไขแล้ว'], true) ? $_POST['status'] : 'เปิด',
'note' => mb_substr(trim((string)($_POST['note'] ?? '')), 0, 4000),
'cat' => mb_substr(trim((string)($_POST['cat'] ?? 'อื่นๆ')), 0, 60),
'updated'=> date('Y-m-d'),
];
if ($row['name'] === '') { echo json_encode(['ok'=>false,'error'=>'ใส่ชื่อปัญหาก่อน'], JSON_UNESCAPED_UNICODE); exit; }
if ($id === '') {
$max = 0; foreach ($bugs as $b) { if (($b['n'] ?? 0) > $max) $max = $b['n']; }
$row['n'] = $max + 1;
$bugs[] = $row;
} else {
$found = false;
foreach ($bugs as &$b) { if ((string)($b['n'] ?? '') === $id) { $b = array_merge($b, $row); $b['n'] = (int)$id; $found = true; break; } }
unset($b);
if (!$found) { echo json_encode(['ok'=>false,'error'=>'ไม่พบบั๊ก #'.$id], JSON_UNESCAPED_UNICODE); exit; }
}
tc_save($BUGS, $bugs);
echo json_encode(['ok'=>true,'bugs'=>$bugs], JSON_UNESCAPED_UNICODE); exit;
}
// ลบบั๊ก
if ($api === 'bugdel') {
$id = preg_replace('/[^0-9]/', '', (string)($_POST['id'] ?? ''));
if ($id === '') { echo json_encode(['ok'=>false,'error'=>'no id'], JSON_UNESCAPED_UNICODE); exit; }
$bugs = tc_load($BUGS);
$bugs = array_values(array_filter($bugs, function($b) use ($id){ return (string)($b['n'] ?? '') !== $id; }));
tc_save($BUGS, $bugs);
echo json_encode(['ok'=>true,'bugs'=>$bugs], JSON_UNESCAPED_UNICODE); exit;
}
// จัดการหมวด — รับ list ทั้งชุด (เพิ่ม/ลบ/แก้/เรียง)
if ($api === 'catsave') {
$list = json_decode((string)($_POST['cats'] ?? '[]'), true);
if (!is_array($list)) { echo json_encode(['ok'=>false,'error'=>'รูปแบบหมวดไม่ถูกต้อง'], JSON_UNESCAPED_UNICODE); exit; }
$clean = [];
foreach ($list as $c) { $c = mb_substr(trim((string)$c), 0, 60); if ($c !== '' && !in_array($c, $clean, true)) $clean[] = $c; }
if (!count($clean)) { echo json_encode(['ok'=>false,'error'=>'ต้องมีอย่างน้อย 1 หมวด'], JSON_UNESCAPED_UNICODE); exit; }
tc_save($CATS, $clean);
echo json_encode(['ok'=>true,'cats'=>$clean], JSON_UNESCAPED_UNICODE); exit;
}
echo json_encode(['ok'=>false,'error'=>'unknown api'], JSON_UNESCAPED_UNICODE); exit;
}
$bugs = tc_load($BUGS);
$verify = tc_load($VERIFY);
$cats = tc_cats($CATS, $DEFAULT_CATS);
?><!DOCTYPE html>
<html lang="th"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Justice — Test Case / Bug List</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg:#0a0f1e;--panel:#121a30;--panel2:#0e1526;--line:#22304f;--txt:#dce6ff;--mut:#8aa0c8;--cyan:#22d3ee;--green:#22c55e;--red:#f87171;--amber:#fbbf24;--violet:#a78bfa;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Kanit,system-ui,sans-serif;background:radial-gradient(circle at 50% -10%,#13213f,#070b16 70%);color:var(--txt);min-height:100vh;padding:24px 16px 80px}
.wrap{max-width:1240px;margin:0 auto}
h1{font-weight:600;font-size:26px;letter-spacing:.5px;margin-bottom:4px}
h1 .sub{color:var(--mut);font-size:14px;font-weight:300;display:block;margin-top:4px}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:12px;margin:18px 0}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.card .v{font-size:26px;font-weight:600}.card .l{font-size:12px;color:var(--mut);margin-top:2px}
.card.ok .v{color:var(--green)}.card.open .v{color:var(--amber)}.card.pass .v{color:var(--cyan)}.card.fail .v{color:var(--red)}
.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:14px 0}
.bar input[type=text],.bar input[type=password]{background:var(--panel2);border:1px solid var(--line);border-radius:10px;color:var(--txt);padding:9px 14px;font-family:inherit;font-size:14px}
#q{flex:1;min-width:180px}
.bar input:focus{outline:none;border-color:var(--cyan)}
.chip{background:var(--panel2);border:1px solid var(--line);border-radius:20px;color:var(--mut);padding:7px 15px;font-size:13px;cursor:pointer;font-family:inherit}
.chip:hover{border-color:var(--cyan);color:var(--txt)}
.chip.on{background:var(--cyan);border-color:var(--cyan);color:#04121a;font-weight:600}
.catbar{display:flex;flex-wrap:wrap;gap:7px;align-items:center;margin:6px 0 12px}
.catbar .lbl{color:var(--mut);font-size:12px;margin-right:2px}
.catchip{background:var(--panel2);border:1px solid var(--line);border-radius:18px;color:var(--mut);padding:6px 13px;font-size:12.5px;cursor:pointer;font-family:inherit}
.catchip:hover{border-color:var(--violet);color:var(--txt)}
.catchip.on{background:var(--violet);border-color:var(--violet);color:#160a2e;font-weight:600}
.catchip .cnt{opacity:.7;font-size:11px;margin-left:3px}
.adminbox{margin-left:auto;display:flex;gap:8px;align-items:center}
.adminbox .ok{color:var(--green);font-size:13px}
.lockhint{font-size:13px;color:var(--amber);white-space:nowrap}
.qabadge{cursor:pointer}.qabadge:hover{filter:brightness(1.15);text-decoration:underline}
@keyframes apFlash{0%,100%{box-shadow:none}30%,70%{box-shadow:0 0 0 3px rgba(248,113,113,.55)}}
#adminpass.flash{animation:apFlash .7s ease}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px}
.btn{border:1px solid var(--line);background:var(--panel2);color:var(--txt);border-radius:9px;padding:7px 14px;font-size:13px;cursor:pointer;font-family:inherit}
.btn:hover{border-color:var(--cyan)}
.btn.primary{background:var(--green);color:#042c14;border-color:var(--green);font-weight:600}
.btn.v{background:rgba(167,139,250,.16);border-color:var(--violet);color:var(--violet)}
table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
th{text-align:left;font-size:12px;color:var(--mut);font-weight:500;padding:11px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}
td{padding:11px 12px;border-bottom:1px solid var(--line);font-size:13.5px;vertical-align:top;line-height:1.45}
tr:last-child td{border-bottom:none}
.n{color:var(--mut);font-size:12px}.nm{font-weight:500}.dt{color:var(--mut);font-size:12.5px;margin-top:3px}.nt{color:#7fd4a8;font-size:12px;margin-top:4px}
.b{display:inline-block;border-radius:6px;padding:3px 9px;font-size:11.5px;font-weight:500;white-space:nowrap}
.cattag{display:inline-block;border-radius:6px;padding:2px 8px;font-size:11px;background:rgba(167,139,250,.16);color:var(--violet);margin-top:5px}
.s-open{background:rgba(251,191,36,.16);color:var(--amber)}.s-fix{background:rgba(34,197,94,.16);color:var(--green)}.s-other{background:rgba(138,160,200,.16);color:var(--mut)}
.p-crit{background:rgba(248,113,113,.16);color:var(--red)}.p-high{background:rgba(251,146,60,.16);color:#fb923c}.p-mid{background:rgba(251,191,36,.14);color:var(--amber)}.p-low{background:rgba(138,160,200,.14);color:var(--mut)}
.qa{min-width:240px}
.r-pass{background:rgba(34,211,238,.16);color:var(--cyan)}.r-fail{background:rgba(248,113,113,.18);color:var(--red)}.r-none{background:rgba(138,160,200,.14);color:var(--mut)}
.qcom{color:#cfe0ff;font-size:12.5px;margin-top:5px;white-space:pre-wrap}
.qat{color:var(--mut);font-size:11px;margin-top:3px}
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}
.thumbs a{display:block;width:54px;height:54px;border-radius:7px;border:1px solid var(--line);background-size:cover;background-position:center;position:relative}
.thumbs .rmimg{position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;background:var(--red);color:#fff;font-size:12px;line-height:18px;text-align:center;cursor:pointer;border:none}
.edit{margin-top:8px;display:none;border-top:1px dashed var(--line);padding-top:8px}
.edit.show{display:block}
.edit .rb{display:inline-flex;gap:6px;margin-bottom:6px}
.edit .rb button{border:1px solid var(--line);background:var(--panel2);color:var(--mut);border-radius:8px;padding:5px 11px;font-size:12.5px;cursor:pointer;font-family:inherit}
.edit .rb button.sel-pass{background:var(--cyan);color:#04121a;border-color:var(--cyan)}
.edit .rb button.sel-fail{background:var(--red);color:#fff;border-color:var(--red)}
.edit .rb button.sel-none{background:#33415c;color:#fff}
.edit textarea,.edit input[type=text],.edit select{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:8px;resize:vertical}
.edit textarea{min-height:46px}
.edit .erow{display:flex;gap:8px;align-items:center;margin-top:6px;flex-wrap:wrap}
.edit .save{background:var(--green);color:#042c14;border:none;border-radius:8px;padding:7px 16px;font-weight:600;cursor:pointer;font-family:inherit}
.edit .msg{font-size:12px}
.editbtn{margin-top:7px;margin-right:6px;background:none;border:1px dashed var(--line);color:var(--mut);border-radius:8px;padding:4px 11px;font-size:12px;cursor:pointer;font-family:inherit}
.editbtn.dev{border-color:var(--amber);color:var(--amber)}
.editbtn.del{border-color:var(--red);color:var(--red)}
.devform{margin-top:8px;display:none;border-top:1px dashed var(--amber);padding-top:8px}
.devform.show{display:block}
.devform .g{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:7px}
.devform label{font-size:11px;color:var(--mut);display:block;margin-bottom:3px}
.foot{color:var(--mut);font-size:12px;margin-top:16px;text-align:center}
.hidden{display:none}
.modal{position:fixed;inset:0;background:rgba(4,8,18,.78);display:none;align-items:center;justify-content:center;z-index:50;padding:16px}
.modal.show{display:flex}
.modal .box{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;max-width:440px;width:100%;max-height:88vh;overflow:auto}
.modal h3{font-weight:600;font-size:17px;margin-bottom:12px}
.catrow{display:flex;gap:7px;align-items:center;margin-bottom:7px}
.catrow input{flex:1;background:var(--panel2);border:1px solid var(--line);border-radius:8px;color:var(--txt);font-family:inherit;font-size:13px;padding:7px 10px}
.catrow .x{background:var(--red);color:#fff;border:none;border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:15px}
@media(max-width:760px){.hide-sm{display:none}td,th{padding:9px 7px}.devform .g{grid-template-columns:1fr}}
</style></head>
<body><div class="wrap">
<h1>Justice — Test Case / Bug List<span class="sub">รายการบัก + ผลตรวจ QA · จัดหมวด · srv1361159.hstgr.cloud/testcase</span></h1>
<div class="toolbar" style="margin:10px 0 4px">
<a class="btn v" href="test.html" style="text-decoration:none">🎮 Test Console — เทสมินิเกม + Disconnect (ปุ่มเดียว)</a>
</div>
<div class="cards" id="cards"></div>
<div class="bar">
<input type="text" id="q" placeholder="ค้นหา… (ชื่อ / รายละเอียด / คำอธิบาย)">
<button class="chip on" data-f="all">ทั้งหมด</button>
<button class="chip" data-f="open">ยังเปิด</button>
<button class="chip" data-f="fixed">แก้แล้ว</button>
<button class="chip" data-f="pass">ผ่าน QA</button>
<button class="chip" data-f="fail">ยังไม่ผ่าน</button>
<button class="chip" data-f="untested">ยังไม่ตรวจ</button>
<span class="adminbox">
<span class="lockhint" id="lockhint">🔒 แก้ไข — ใส่รหัส:</span>
<input type="password" id="adminpass" placeholder="รหัสแอดมิน" autocomplete="off">
<span id="adminok" class="ok hidden">โหมดแก้ไข ✓</span>
</span>
</div>
<div class="catbar" id="catbar"></div>
<div class="toolbar hidden" id="toolbar">
<button class="btn primary" id="addBug">+ เพิ่มบั๊ก</button>
<button class="btn v" id="mgCats">⚙ จัดการหมวด</button>
</div>
<table><thead><tr><th style="width:30px">#</th><th>ปัญหา</th><th style="width:78px">สำคัญ</th><th style="width:88px">Dev</th><th class="qa">ผลตรวจ QA</th></tr></thead>
<tbody id="tb"></tbody></table>
<div class="foot">Justice bug tracker · ผลตรวจ+Dev แก้ไขได้เมื่อใส่รหัสแอดมิน · <span id="cnt"><?php echo count($bugs); ?></span> รายการ</div>
</div>
<div class="modal" id="catModal"><div class="box">
<h3>จัดการหมวด</h3>
<div id="catList"></div>
<button class="btn" id="catAdd">+ เพิ่มหมวด</button>
<div class="erow" style="margin-top:14px;display:flex;gap:8px"><button class="btn primary" id="catSave">บันทึก</button><button class="btn" id="catClose">ปิด</button><span class="msg" id="catMsg" style="font-size:12px"></span></div>
</div></div>
<script>
var BUGS = <?php echo json_encode($bugs, JSON_UNESCAPED_UNICODE); ?>;
var VERIFY = <?php echo json_encode((object)$verify, JSON_UNESCAPED_UNICODE); ?>;
var CATS = <?php echo json_encode($cats, JSON_UNESCAPED_UNICODE); ?>;
var filt="all",q="",adminPass="",catFilt="";
function esc(s){return (s==null?"":(""+s)).replace(/[&<>"]/g,function(c){return{"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c]})}
function sCls(s){return s.indexOf("แก้")>=0?"s-fix":s==="เปิด"?"s-open":"s-other"}
function pCls(p){return p.indexOf("วิกฤต")>=0?"p-crit":p.indexOf("สูง")>=0?"p-high":p.indexOf("กลาง")>=0?"p-mid":p?"p-low":""}
function vof(n){return VERIFY[n]||{result:"none",comment:"",images:[]}}
function bcat(b){return (b.cat&&(""+b.cat).trim())?(""+b.cat).trim():"อื่นๆ"}
function rLabel(r){return r==="pass"?"✅ เช็คแล้ว (ผ่าน)":r==="fail"?"❌ ยังไม่ผ่าน":"⬜ ยังไม่ตรวจ"}
function rCls(r){return r==="pass"?"r-pass":r==="fail"?"r-fail":"r-none"}
function counts(){
var o=0,f=0,p=0,fa=0,un=0;
BUGS.forEach(function(b){if(b.status==="เปิด")o++;if((b.status||"").indexOf("แก้")>=0)f++;var v=vof(b.n);if(v.result==="pass")p++;else if(v.result==="fail")fa++;else un++;});
return {o:o,f:f,p:p,fa:fa,un:un};
}
function catBar(){
var byCat={}; BUGS.forEach(function(b){var c=bcat(b);byCat[c]=(byCat[c]||0)+1;});
var html='<span class="lbl">หมวด:</span><button class="catchip'+(catFilt===""?" on":"")+'" data-cat="">ทั้งหมด <span class="cnt">'+BUGS.length+'</span></button>';
CATS.forEach(function(c){html+='<button class="catchip'+(catFilt===c?" on":"")+'" data-cat="'+esc(c)+'">'+esc(c)+' <span class="cnt">'+(byCat[c]||0)+'</span></button>';});
// หมวดที่บั๊กใช้แต่ไม่อยู่ในรายการ (กันตกหล่น)
Object.keys(byCat).forEach(function(c){if(CATS.indexOf(c)<0){html+='<button class="catchip'+(catFilt===c?" on":"")+'" data-cat="'+esc(c)+'" style="opacity:.7">'+esc(c)+' <span class="cnt">'+byCat[c]+'</span></button>';}});
document.getElementById("catbar").innerHTML=html;
}
function render(){
var c=counts();
document.getElementById("cards").innerHTML=
'<div class="card"><div class="v">'+BUGS.length+'</div><div class="l">ทั้งหมด</div></div>'+
'<div class="card open"><div class="v">'+c.o+'</div><div class="l">Dev: ยังเปิด</div></div>'+
'<div class="card ok"><div class="v">'+c.f+'</div><div class="l">Dev: แก้แล้ว</div></div>'+
'<div class="card pass"><div class="v">'+c.p+'</div><div class="l">QA: ผ่าน</div></div>'+
'<div class="card fail"><div class="v">'+c.fa+'</div><div class="l">QA: ยังไม่ผ่าน</div></div>'+
'<div class="card"><div class="v">'+c.un+'</div><div class="l">QA: ยังไม่ตรวจ</div></div>';
catBar();
document.getElementById("toolbar").classList.toggle("hidden",!adminPass);
document.getElementById("cnt").textContent=BUGS.length;
var list=BUGS.filter(function(b){
var v=vof(b.n);
if(catFilt&&bcat(b)!==catFilt)return false;
if(filt==="open"&&b.status!=="เปิด")return false;
if(filt==="fixed"&&(b.status||"").indexOf("แก้")<0)return false;
if(filt==="pass"&&v.result!=="pass")return false;
if(filt==="fail"&&v.result!=="fail")return false;
if(filt==="untested"&&v.result&&v.result!=="none")return false;
if(q){var h=(b.name+" "+b.detail+" "+b.note+" "+bcat(b)+" "+(v.comment||"")).toLowerCase();if(h.indexOf(q)<0)return false}
return true;});
document.getElementById("tb").innerHTML=list.map(rowHtml).join("")||'<tr><td colspan="5" style="text-align:center;color:var(--mut);padding:30px">ไม่พบรายการ</td></tr>';
}
function thumbsHtml(v,n,admin){
return (v.images||[]).map(function(im){
var rm=admin?'<button class="rmimg" data-rm="'+esc(im)+'" data-id="'+n+'" title="ลบรูป">×</button>':'';
return '<a href="uploads/'+encodeURIComponent(im)+'" target="_blank" style="background-image:url(uploads/'+encodeURIComponent(im)+')">'+rm+'</a>';
}).join("");
}
function catOptions(sel){var o="";CATS.forEach(function(c){o+='<option'+(c===sel?" selected":"")+'>'+esc(c)+'</option>';});if(CATS.indexOf(sel)<0&&sel){o+='<option selected>'+esc(sel)+'</option>';}return o;}
function rowHtml(b){
var v=vof(b.n),admin=!!adminPass;
var qa='<span class="b '+rCls(v.result)+' qabadge" data-qa="'+b.n+'" title="'+(admin?'คลิกเพื่อแก้ผลตรวจ':'ใส่รหัสแอดมินมุมขวาบนเพื่อแก้')+'">'+rLabel(v.result)+'</span>'+
(v.comment?'<div class="qcom">'+esc(v.comment)+'</div>':'')+
(v.at?'<div class="qat">อัปเดต '+esc(v.at)+'</div>':'')+
'<div class="thumbs">'+thumbsHtml(v,b.n,admin)+'</div>';
if(admin){
qa+='<button class="editbtn" data-edit="'+b.n+'">✎ ผลตรวจ</button>'+
'<button class="editbtn dev" data-dev="'+b.n+'">🛠 Dev/หมวด</button>'+
'<button class="editbtn del" data-del="'+b.n+'">🗑 ลบ</button>'+
'<div class="edit" id="edit-'+b.n+'">'+
'<div class="rb" data-rbid="'+b.n+'">'+
'<button data-r="pass" class="'+(v.result==="pass"?"sel-pass":"")+'">ผ่าน</button>'+
'<button data-r="fail" class="'+(v.result==="fail"?"sel-fail":"")+'">ยังไม่ผ่าน</button>'+
'<button data-r="none" class="'+(v.result==="none"||!v.result?"sel-none":"")+'">ยังไม่ตรวจ</button>'+
'</div>'+
'<textarea id="com-'+b.n+'" placeholder="คำอธิบายผลตรวจ…">'+esc(v.comment||"")+'</textarea>'+
'<div class="erow"><input type="file" id="img-'+b.n+'" accept="image/*">'+
'<button class="save" data-save="'+b.n+'">บันทึก</button>'+
'<span class="msg" id="msg-'+b.n+'"></span></div>'+
'</div>'+
'<div class="devform" id="dev-'+b.n+'">'+
'<div class="g">'+
'<div><label>หมวด</label><select id="d-cat-'+b.n+'">'+catOptions(bcat(b))+'</select></div>'+
'<div><label>Dev สถานะ</label><select id="d-status-'+b.n+'"><option'+(b.status==="เปิด"?" selected":"")+'>เปิด</option><option'+((b.status||"").indexOf("แก้")>=0?" selected":"")+'>แก้ไขแล้ว</option></select></div>'+
'<div><label>ความสำคัญ</label><select id="d-prio-'+b.n+'"><option value="">-</option>'+["วิกฤต","สูง","กลาง","ต่ำ"].map(function(p){return '<option'+(b.prio===p?" selected":"")+'>'+p+'</option>';}).join("")+'</select></div>'+
'<div><label>ชื่อปัญหา</label><input type="text" id="d-name-'+b.n+'" value="'+esc(b.name)+'"></div>'+
'</div>'+
'<label>รายละเอียด</label><textarea id="d-detail-'+b.n+'">'+esc(b.detail||"")+'</textarea>'+
'<label style="margin-top:6px">หมายเหตุ Dev (วิธีแก้)</label><textarea id="d-note-'+b.n+'">'+esc(b.note||"")+'</textarea>'+
'<div class="erow"><button class="save" data-bugsave="'+b.n+'">บันทึก Dev</button><span class="msg" id="dmsg-'+b.n+'"></span></div>'+
'</div>';
}
return '<tr><td class="n">'+b.n+'</td><td><div class="nm">'+esc(b.name)+'</div>'+
(b.detail?'<div class="dt">'+esc(b.detail)+'</div>':'')+
(b.note?'<div class="nt">'+esc(b.note)+'</div>':'')+
'<div class="cattag">'+esc(bcat(b))+'</div></td>'+
'<td>'+(b.prio?'<span class="b '+pCls(b.prio)+'">'+esc(b.prio)+'</span>':'')+'</td>'+
'<td><span class="b '+sCls(b.status)+'">'+esc(b.status)+'</span></td>'+
'<td class="qa">'+qa+'</td></tr>';
}
function flashAdmin(){var a=document.getElementById("adminpass");a.classList.remove("flash");void a.offsetWidth;a.classList.add("flash");a.focus();}
document.getElementById("tb").addEventListener("click",function(e){
var t=e.target;
if(t.dataset.qa){ if(!adminPass){ flashAdmin(); } else { var ee=document.getElementById("edit-"+t.dataset.qa); if(ee) ee.classList.toggle("show"); } return; }
if(t.dataset.edit){var ed=document.getElementById("edit-"+t.dataset.edit);if(ed)ed.classList.toggle("show");return;}
if(t.dataset.dev){var df=document.getElementById("dev-"+t.dataset.dev);if(df)df.classList.toggle("show");return;}
if(t.dataset.del){doDelBug(t.dataset.del);return;}
if(t.dataset.r){var rb=t.parentNode;[].forEach.call(rb.children,function(x){x.className=""});t.className="sel-"+t.dataset.r;rb.dataset.sel=t.dataset.r;return;}
if(t.dataset.save){doSave(t.dataset.save);return;}
if(t.dataset.bugsave){doBugSave(t.dataset.bugsave);return;}
if(t.dataset.rm){doRemoveImg(t.dataset.id,t.dataset.rm);return;}
});
function curResult(n){var rb=document.querySelector('.rb[data-rbid="'+n+'"]');if(rb&&rb.dataset.sel)return rb.dataset.sel;var v=vof(n);return v.result||"none";}
function doSave(n){
var fd=new FormData();fd.append("api","save");fd.append("pass",adminPass);fd.append("id",n);
fd.append("result",curResult(n));fd.append("comment",document.getElementById("com-"+n).value);
var f=document.getElementById("img-"+n);if(f&&f.files&&f.files[0])fd.append("image",f.files[0]);
var msg=document.getElementById("msg-"+n);msg.textContent="กำลังบันทึก…";msg.style.color="var(--mut)";
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){
if(j.ok){VERIFY[n]=j.rec;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";setTimeout(render,500);}
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
}
function doBugSave(n){
var fd=new FormData();fd.append("api","bugsave");fd.append("pass",adminPass);fd.append("id",n);
fd.append("name",document.getElementById("d-name-"+n).value);
fd.append("detail",document.getElementById("d-detail-"+n).value);
fd.append("note",document.getElementById("d-note-"+n).value);
fd.append("cat",document.getElementById("d-cat-"+n).value);
fd.append("status",document.getElementById("d-status-"+n).value);
fd.append("prio",document.getElementById("d-prio-"+n).value);
var msg=document.getElementById("dmsg-"+n);msg.textContent="กำลังบันทึก…";msg.style.color="var(--mut)";
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){
if(j.ok){BUGS=j.bugs;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";setTimeout(render,400);}
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
}
function doDelBug(n){
if(!confirm("ลบบั๊ก #"+n+" ?"))return;
var fd=new FormData();fd.append("api","bugdel");fd.append("pass",adminPass);fd.append("id",n);
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){BUGS=j.bugs;render();}else alert(j.error||"ลบไม่ได้");});
}
function doRemoveImg(n,im){
if(!confirm("ลบรูปนี้?"))return;
var fd=new FormData();fd.append("api","save");fd.append("pass",adminPass);fd.append("id",n);fd.append("removeImage",im);
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){VERIFY[n]=j.rec;render();}});
}
// เพิ่มบั๊กใหม่
document.getElementById("addBug").addEventListener("click",function(){
var name=prompt("ชื่อปัญหา (บั๊กใหม่):");if(!name||!name.trim())return;
var cat=prompt("หมวด (เช่น "+CATS.slice(0,3).join(" / ")+"):",catFilt||"อื่นๆ")||"อื่นๆ";
var fd=new FormData();fd.append("api","bugsave");fd.append("pass",adminPass);fd.append("id","");
fd.append("name",name.trim());fd.append("cat",cat.trim());fd.append("status","เปิด");fd.append("prio","กลาง");
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){if(j.ok){BUGS=j.bugs;render();}else alert(j.error||"เพิ่มไม่ได้");});
});
// จัดการหมวด
var catModal=document.getElementById("catModal");
function renderCatList(){
document.getElementById("catList").innerHTML=CATS.map(function(c,i){
return '<div class="catrow"><input type="text" value="'+esc(c)+'" data-ci="'+i+'"><button class="x" data-cx="'+i+'">×</button></div>';
}).join("");
}
document.getElementById("mgCats").addEventListener("click",function(){renderCatList();document.getElementById("catMsg").textContent="";catModal.classList.add("show");});
document.getElementById("catClose").addEventListener("click",function(){catModal.classList.remove("show");});
document.getElementById("catAdd").addEventListener("click",function(){CATS=collectCats();CATS.push("หมวดใหม่");renderCatList();});
document.getElementById("catList").addEventListener("click",function(e){if(e.target.dataset.cx!=null){CATS=collectCats();CATS.splice(+e.target.dataset.cx,1);renderCatList();}});
function collectCats(){var out=[];[].forEach.call(document.querySelectorAll("#catList input"),function(inp){var v=inp.value.trim();if(v&&out.indexOf(v)<0)out.push(v);});return out;}
document.getElementById("catSave").addEventListener("click",function(){
var list=collectCats();if(!list.length){document.getElementById("catMsg").textContent="ต้องมีอย่างน้อย 1 หมวด";document.getElementById("catMsg").style.color="var(--red)";return;}
var fd=new FormData();fd.append("api","catsave");fd.append("pass",adminPass);fd.append("cats",JSON.stringify(list));
var msg=document.getElementById("catMsg");msg.textContent="กำลังบันทึก…";msg.style.color="var(--mut)";
fetch(location.pathname,{method:"POST",body:fd}).then(function(r){return r.json()}).then(function(j){
if(j.ok){CATS=j.cats;msg.textContent="บันทึกแล้ว ✓";msg.style.color="var(--green)";render();setTimeout(function(){catModal.classList.remove("show");},600);}
else{msg.textContent=j.error||"ผิดพลาด";msg.style.color="var(--red)";}
}).catch(function(){msg.textContent="เชื่อมต่อไม่ได้";msg.style.color="var(--red)";});
});
// admin pass
var ap=document.getElementById("adminpass");
ap.value=localStorage.getItem("tcAdminPass")||"";
function applyAdmin(){adminPass=ap.value.trim();localStorage.setItem("tcAdminPass",adminPass);
document.getElementById("adminok").classList.toggle("hidden",!adminPass);
document.getElementById("lockhint").classList.toggle("hidden",!!adminPass);render();}
ap.addEventListener("input",applyAdmin);applyAdmin();
document.getElementById("q").addEventListener("input",function(e){q=e.target.value.toLowerCase().trim();render()});
[].forEach.call(document.querySelectorAll(".chip"),function(b){b.addEventListener("click",function(){
[].forEach.call(document.querySelectorAll(".chip"),function(x){x.classList.remove("on")});
b.classList.add("on");filt=b.getAttribute("data-f");render();})});
document.getElementById("catbar").addEventListener("click",function(e){var t=e.target.closest(".catchip");if(!t)return;catFilt=t.getAttribute("data-cat")||"";render();});
render();
</script></body></html>