429 lines
32 KiB
PHP
429 lines
32 KiB
PHP
<?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{"&":"&","<":"<",">":">","\"":"""}[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>
|