ผังโรงเรียน
คลิกบนแผนผังเพื่อแจ้งจุดใหม่
<g fill="none" stroke="var(--blueprint-line)" stroke-width="2.5">
<rect data-zone="ร้านค้าสวัสดิการ" x="40" y="30" width="180" height="90" rx="6" fill="#3A4448"/>
<rect data-zone="อาคาร 9" x="340" y="30" width="280" height="100" rx="6" fill="#274A5C"/>
<rect data-zone="ห้องสมุด" x="760" y="30" width="200" height="90" rx="6" fill="#2E5566"/>
<rect data-zone="อาคาร 1" x="40" y="150" width="220" height="180" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 5" x="300" y="150" width="160" height="180" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 2" x="500" y="150" width="220" height="180" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 6" x="760" y="150" width="200" height="180" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 7" x="40" y="350" width="160" height="380" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 3" x="240" y="350" width="200" height="110" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 11" x="480" y="350" width="200" height="110" rx="6" fill="#274A5C"/>
<rect data-zone="อาคาร 8" x="800" y="350" width="160" height="380" rx="6" fill="#274A5C"/>
<rect data-zone="สนามฟุตบอล" x="240" y="480" width="440" height="200" rx="6" fill="#20443A"/>
<rect data-zone="ลานจอดรถ" x="40" y="740" width="160" height="110" rx="6" fill="#3D4448"/>
<rect data-zone="มินิมาร์ท" x="240" y="700" width="200" height="90" rx="6" fill="#3A4448"/>
<rect data-zone="MDD" x="460" y="700" width="200" height="90" rx="6" fill="#3A4448"/>
<rect data-zone="ลานจอดรถ" x="800" y="740" width="160" height="110" rx="6" fill="#3D4448"/>
<rect data-zone="อาคาร 10" x="240" y="810" width="200" height="90" rx="6" fill="#274A5C"/>
<rect data-zone="ห้องพยาบาล" x="460" y="810" width="200" height="90" rx="6" fill="#3A4448"/>
<rect data-zone="Resource" x="680" y="810" width="180" height="90" rx="6" fill="#2E5566"/>
</g>
<g fill="var(--blueprint-line)" font-family="Sarabun, sans-serif" font-size="16" font-weight="600">
<text x="130" y="80" text-anchor="middle" font-size="13">ร้านค้าสวัสดิการ</text>
<text x="480" y="85" text-anchor="middle">อาคาร 9</text>
<text x="860" y="80" text-anchor="middle">ห้องสมุด</text>
<text x="150" y="245" text-anchor="middle">อาคาร 1</text>
<text x="150" y="265" text-anchor="middle" font-size="12" fill="#AFC4CC">(ห้องแล็บส่วนใหญ่)</text>
<text x="380" y="245" text-anchor="middle">อาคาร 5</text>
<text x="610" y="245" text-anchor="middle">อาคาร 2</text>
<text x="860" y="245" text-anchor="middle">อาคาร 6</text>
<text x="120" y="545" text-anchor="middle">อาคาร 7</text>
<text x="340" y="410" text-anchor="middle">อาคาร 3</text>
<text x="580" y="410" text-anchor="middle">อาคาร 11</text>
<text x="880" y="545" text-anchor="middle">อาคาร 8</text>
<text x="460" y="585" text-anchor="middle" font-size="18">สนามฟุตบอล</text>
<text x="120" y="800" text-anchor="middle" font-size="14">ลานจอดรถ</text>
<text x="340" y="750" text-anchor="middle" font-size="14">มินิมาร์ท</text>
<text x="560" y="750" text-anchor="middle" font-size="14">MDD</text>
<text x="880" y="800" text-anchor="middle" font-size="14">ลานจอดรถ</text>
<text x="340" y="860" text-anchor="middle">อาคาร 10</text>
<text x="560" y="860" text-anchor="middle" font-size="14">ห้องพยาบาล</text>
<text x="770" y="860" text-anchor="middle" font-size="14">Resource</text>
</g>
<g transform="translate(940,60)" stroke="var(--blueprint-line)" fill="var(--blueprint-line)">
<line x1="0" y1="-18" x2="0" y2="18" stroke-width="1.5"/>
<polygon points="0,-18 5,-6 -5,-6"/>
<text x="0" y="-24" text-anchor="middle" font-size="13" font-family="Sarabun, sans-serif" stroke="none">N</text>
</g>
</svg>
<div id="pinLayer"></div>
</div>
<p class="map-hint" id="mapHint">แตะหรือคลิกตำแหน่งบนแผนผังเพื่อเริ่มแจ้งเหตุ</p>
</div>
<div class="sidebar">
<div class="filters" id="filterRow"></div>
<div class="report-list" id="reportList">
<div class="skeleton">กำลังโหลดข้อมูล…</div>
</div>
<div class="admin-row">
<button id="clearAllBtn" type="button">ล้างข้อมูลทั้งหมด</button>
</div>
</div>
แจ้งจุดใหม่
บริเวณ: —
<div class="type-toggle">
<button type="button" class="hazard" id="btnTypeHazard">⚠ จุดเสี่ยง</button>
<button type="button" class="dirty" id="btnTypeDirty">🧹 จุดสกปรก</button>
</div>
<div id="sevBlock" style="display:none;">
<label>ระดับความเร่งด่วน</label>
<div class="sev-row">
<button type="button" data-sev="low">ต่ำ</button>
<button type="button" data-sev="medium">กลาง</button>
<button type="button" data-sev="high">สูง</button>
</div>
</div>
<div class="row-2">
<div>
<label for="floorInput">ชั้น (ไม่บังคับ)</label>
<input type="text" id="floorInput" placeholder="เช่น 2">
</div>
<div>
<label for="nameInput">ชื่อผู้แจ้ง (ไม่บังคับ)</label>
<input type="text" id="nameInput" placeholder="ไม่ระบุก็ได้">
</div>
</div>
<label for="detailInput">รายละเอียด</label>
<textarea id="detailInput" placeholder="เช่น พื้นกระเบื้องแตก อาจสะดุดล้มได้ / ถังขยะล้นและมีกลิ่น"></textarea>
<div class="form-actions">
<button class="btn btn-cancel" id="cancelBtn" type="button">ยกเลิก</button>
<button class="btn btn-submit" id="submitBtn" type="button" disabled>ส่งเรื่องแจ้ง</button>
</div>
<script>
(function(){
const ZONES = [
{name:"ร้านค้าสวัสดิการ", x:40, y:30, w:180, h:90},
{name:"อาคาร 9", x:340, y:30, w:280, h:100},
{name:"ห้องสมุด", x:760, y:30, w:200, h:90},
{name:"อาคาร 1", x:40, y:150, w:220, h:180},
{name:"อาคาร 5", x:300, y:150, w:160, h:180},
{name:"อาคาร 2", x:500, y:150, w:220, h:180},
{name:"อาคาร 6", x:760, y:150, w:200, h:180},
{name:"อาคาร 7", x:40, y:350, w:160, h:380},
{name:"อาคาร 3", x:240, y:350, w:200, h:110},
{name:"อาคาร 11", x:480, y:350, w:200, h:110},
{name:"อาคาร 8", x:800, y:350, w:160, h:380},
{name:"สนามฟุตบอล", x:240, y:480, w:440, h:200},
{name:"ลานจอดรถ", x:40, y:740, w:160, h:110},
{name:"มินิมาร์ท", x:240, y:700, w:200, h:90},
{name:"MDD", x:460, y:700, w:200, h:90},
{name:"ลานจอดรถ", x:800, y:740, w:160, h:110},
{name:"อาคาร 10", x:240, y:810, w:200, h:90},
{name:"ห้องพยาบาล", x:460, y:810, w:200, h:90},
{name:"Resource", x:680, y:810, w:180, h:90},
];
const ZONE_NOTES = {
"อาคาร 1": "อาคารนี้ส่วนใหญ่เป็นห้องแล็บ",
"อาคาร 5": "ห้องน้ำหญิง: ชั้น 2 และ 4 · ห้องน้ำชาย: ชั้นอื่น",
"อาคาร 7": "ห้องน้ำหญิง: ชั้น 2 และ 4 · ห้องน้ำชาย: ชั้น 3 และ 5",
"อาคาร 10": "มีห้องน้ำหญิงและชายที่ชั้น 2",
"อาคาร 11": "มีห้องน้ำหญิงและชายที่ชั้น 2",
};
const STORAGE_KEY = "campus-reports";
const FILTERS = [
{id:"all", label:"ทั้งหมด"},
{id:"hazard", label:"จุดเสี่ยง"},
{id:"dirty", label:"จุดสกปรก"},
{id:"resolved", label:"แก้ไขแล้ว"},
];
let reports = [];
let activeFilter = "all";
let pendingPoint = null;
let formType = "hazard";
let formSev = "medium";
let loaded = false;
let deleteConfirmId = null;
const el = (id) => document.getElementById(id);
const mapWrap = el("mapWrap");
const pinLayer = el("pinLayer");
const reportListEl = el("reportList");
const filterRow = el("filterRow");
const statRow = el("statRow");
const errorBanner = el("errorBanner");
function zoneAt(xPct, yPct){
const x = xPct*1000, y = yPct*900;
for(const z of ZONES){
if(x>=z.x && x<=z.x+z.w && y>=z.y && y<=z.y+z.h) return z.name;
}
return "พื้นที่ทั่วไป";
}
function relTimeThai(ts){
const diffMs = Date.now() - ts;
const min = Math.floor(diffMs/60000);
if(min < 1) return "เมื่อสักครู่";
if(min < 60) return min + " นาทีที่แล้ว";
const hr = Math.floor(min/60);
if(hr < 24) return hr + " ชั่วโมงที่แล้ว";
const day = Math.floor(hr/24);
if(day < 7) return day + " วันที่แล้ว";
return new Date(ts).toLocaleDateString("th-TH", {day:"numeric", month:"short"});
}
function showError(msg){ errorBanner.innerHTML = '
'+msg+'
'; }
function clearError(){ errorBanner.innerHTML = ""; }
function showToast(msg){
const t = document.createElement("div");
t.className = "toast";
t.textContent = msg;
document.body.appendChild(t);
setTimeout(()=>t.remove(), 2200);
}
async function loadReports(){
try{
const res = await window.storage.get(STORAGE_KEY, true);
reports = res && res.value ? JSON.parse(res.value) : [];
}catch(e){
reports = [];
}
loaded = true;
render();
}
async function saveReports(){
try{
const result = await window.storage.set(STORAGE_KEY, JSON.stringify(reports), true);
if(!result){ showError("บันทึกข้อมูลไม่สำเร็จ กรุณาลองใหม่อีกครั้ง"); return false; }
clearError();
return true;
}catch(e){
showError("เกิดข้อผิดพลาดในการบันทึกข้อมูล การเปลี่ยนแปลงอาจไม่ถูกบันทึก");
return false;
}
}
function renderStats(){
const openHazard = reports.filter(r=>r.type==="hazard" && r.status==="open").length;
const openDirty = reports.filter(r=>r.type==="dirty" && r.status==="open").length;
const resolved = reports.filter(r=>r.status==="resolved").length;
statRow.innerHTML = `
จุดเสี่ยงที่ยังไม่แก้ ${openHazard}
จุดสกปรกที่รอทำความสะอาด ${openDirty}
แก้ไขแล้ว ${resolved}
`;
}
function renderFilters(){
filterRow.innerHTML = "";
FILTERS.forEach(f=>{
const btn = document.createElement("button");
btn.className = "filter-btn" + (activeFilter===f.id ? " active" : "");
btn.textContent = f.label;
btn.onclick = ()=>{ activeFilter = f.id; render(); };
filterRow.appendChild(btn);
});
}
function filteredReports(){
return reports.filter(r=>{
if(activeFilter==="all") return true;
if(activeFilter==="resolved") return r.status==="resolved";
return r.type===activeFilter && r.status==="open";
}).sort((a,b)=>b.createdAt-a.createdAt);
}
function escapeHtml(s){
const d = document.createElement("div");
d.textContent = s;
return d.innerHTML;
}
function renderList(){
const list = filteredReports();
if(!loaded){
reportListEl.innerHTML = '
กำลังโหลดข้อมูล…
';
return;
}
if(list.length===0){
reportListEl.innerHTML = '
ยังไม่มีรายการในหมวดนี้
คลิกที่แผนผังเพื่อแจ้งจุดแรก
';
return;
}
reportListEl.innerHTML = "";
list.forEach(r=>{
const card = document.createElement("div");
card.className = "report-card type-"+r.type + (r.status==="resolved" ? " status-resolved" : "");
const badgeType = r.type==="hazard"
? '
จุดเสี่ยง'+(r.severity ? " · "+({low:"ต่ำ",medium:"กลาง",high:"สูง"}[r.severity]) : "")+''
: '
จุดสกปรก';
const badgeStatus = r.status==="resolved" ? '
แก้ไขแล้ว' : "";
const zoneLabel = r.zone + (r.floor ? " (ชั้น "+escapeHtml(r.floor)+")" : "");
card.innerHTML = `
${zoneLabel}
${relTimeThai(r.createdAt)}
${badgeType}${badgeStatus}
${escapeHtml(r.detail)}${r.reporter ? '
แจ้งโดย '+escapeHtml(r.reporter)+'' : ""}
`;
const actions = card.querySelector(".rc-actions");
if(r.status==="open"){
const resolveBtn = document.createElement("button");
resolveBtn.className = "rc-btn resolve";
resolveBtn.textContent = "✓ ทำเครื่องหมายว่าแก้ไขแล้ว";
resolveBtn.onclick = ()=>updateStatus(r.id, "resolved");
actions.appendChild(resolveBtn);
} else {
const reopenBtn = document.createElement("button");
reopenBtn.className = "rc-btn reopen";
reopenBtn.textContent = "เปิดใหม่อีกครั้ง";
reopenBtn.onclick = ()=>updateStatus(r.id, "open");
actions.appendChild(reopenBtn);
}
const delBtn = document.createElement("button");
delBtn.className = "rc-btn delete" + (deleteConfirmId===r.id ? " confirming" : "");
delBtn.textContent = deleteConfirmId===r.id ? "ยืนยันการลบ?" : "ลบ";
delBtn.onclick = ()=>{
if(deleteConfirmId===r.id){ deleteReport(r.id); deleteConfirmId=null; }
else { deleteConfirmId = r.id; renderList(); }
};
actions.appendChild(delBtn);
reportListEl.appendChild(card);
});
}
function renderPins(){
pinLayer.innerHTML = "";
reports.forEach(r=>{
const pin = document.createElement("button");
let cls = "pin type-"+r.type;
if(r.status==="resolved") cls += " status-resolved";
if(r.type==="hazard") cls += " sev-"+(r.severity||"medium");
const matchesFilter = activeFilter==="all" ||
(activeFilter==="resolved" ? r.status==="resolved" : (r.type===activeFilter && r.status==="open"));
if(!matchesFilter) cls += " dimmed";
pin.className = cls;
pin.style.left = (r.x*100)+"%";
pin.style.top = (r.y*100)+"%";
pin.title = r.zone+": "+r.detail;
pin.onclick = (e)=>{ e.stopPropagation(); scrollToCardAndHighlight(r); };
pinLayer.appendChild(pin);
});
if(pendingPoint){
const marker = document.createElement("div");
marker.className = "new-pin-marker";
marker.style.left = (pendingPoint.xPct*100)+"%";
marker.style.top = (pendingPoint.yPct*100)+"%";
pinLayer.appendChild(marker);
}
}
function scrollToCardAndHighlight(r){
activeFilter = "all";
renderFilters();
render();
showToast(r.zone+": "+(r.detail.length>40 ? r.detail.slice(0,40)+"…" : r.detail));
}
function render(){
renderStats();
renderFilters();
renderList();
renderPins();
}
async function updateStatus(id, status){
reports = reports.map(r=> r.id===id ? {...r, status} : r);
render();
await saveReports();
}
async function deleteReport(id){
reports = reports.filter(r=>r.id!==id);
render();
await saveReports();
}
mapWrap.addEventListener("click", (e)=>{
const rect = mapWrap.getBoundingClientRect();
const xPct = (e.clientX - rect.left) / rect.width;
const yPct = (e.clientY - rect.top) / rect.height;
pendingPoint = {xPct, yPct, zone: zoneAt(xPct,yPct)};
renderPins();
openForm();
});
const formOverlay = el("formOverlay");
const formZoneLine = el("formZoneLine");
const zoneNote = el("zoneNote");
const btnTypeHazard = el("btnTypeHazard");
const btnTypeDirty = el("btnTypeDirty");
const sevBlock = el("sevBlock");
const detailInput = el("detailInput");
const nameInput = el("nameInput");
const floorInput = el("floorInput");
const submitBtn = el("submitBtn");
function openForm(){
formType = "hazard";
formSev = "medium";
detailInput.value = "";
nameInput.value = "";
floorInput.value = "";
formZoneLine.textContent = "บริเวณ: " + pendingPoint.zone;
const note = ZONE_NOTES[pendingPoint.zone];
if(note){ zoneNote.style.display = "block"; zoneNote.textContent = note; }
else { zoneNote.style.display = "none"; }
updateTypeButtons();
updateSevButtons();
updateSubmitState();
formOverlay.classList.add("open");
}
function closeForm(){
formOverlay.classList.remove("open");
pendingPoint = null;
renderPins();
}
function updateTypeButtons(){
btnTypeHazard.classList.toggle("active", formType==="hazard");
btnTypeDirty.classList.toggle("active", formType==="dirty");
sevBlock.style.display = formType==="hazard" ? "block" : "none";
}
function updateSevButtons(){
sevBlock.querySelectorAll("button").forEach(b=>{
b.classList.toggle("active", b.dataset.sev===formSev);
});
}
function updateSubmitState(){
submitBtn.disabled = detailInput.value.trim().length===0;
}
btnTypeHazard.onclick = ()=>{ formType="hazard"; updateTypeButtons(); };
btnTypeDirty.onclick = ()=>{ formType="dirty"; updateTypeButtons(); };
sevBlock.querySelectorAll("button").forEach(b=>{
b.onclick = ()=>{ formSev = b.dataset.sev; updateSevButtons(); };
});
detailInput.addEventListener("input", updateSubmitState);
el("cancelBtn").onclick = closeForm;
formOverlay.addEventListener("click", (e)=>{ if(e.target===formOverlay) closeForm(); });
submitBtn.onclick = async ()=>{
if(!pendingPoint || detailInput.value.trim().length===0) return;
const newReport = {
id: "r_" + Date.now() + "_" + Math.random().toString(36).slice(2,7),
type: formType,
severity: formType==="hazard" ? formSev : null,
detail: detailInput.value.trim(),
reporter: nameInput.value.trim(),
floor: floorInput.value.trim(),
zone: pendingPoint.zone,
x: pendingPoint.xPct,
y: pendingPoint.yPct,
status: "open",
createdAt: Date.now(),
};
reports.push(newReport);
closeForm();
render();
showToast("แจ้งเหตุเรียบร้อยแล้ว");
await saveReports();
};
el("clearAllBtn").onclick = async ()=>{
if(el("clearAllBtn").textContent === "ยืนยันการล้างข้อมูลทั้งหมด?"){
reports = [];
el("clearAllBtn").textContent = "ล้างข้อมูลทั้งหมด";
render();
await saveReports();
} else {
el("clearAllBtn").textContent = "ยืนยันการล้างข้อมูลทั้งหมด?";
setTimeout(()=>{ el("clearAllBtn").textContent = "ล้างข้อมูลทั้งหมด"; }, 3000);
}
};
render();
loadReports();
})();
</script>
HTMLEOF
cp /home/claude/school-hazard-map.html /mnt/user-data/outputs/school-hazard-map.html
echo done
cat > /home/claude/school-hazard-map.html << 'HTMLEOF'
<title>แผนผังแจ้งจุดเสี่ยงและจุดสกปรก — โรงเรียนขอนแก่นวิทยายน</title> <style> :root{ --paper:#F1EAD6; --paper-dim:#E6DCC2; --ink:#20302B; --ink-soft:#5B6B62; --blueprint:#1C3A4A; --blueprint-line:#F1EAD6; --blueprint-dim:#2A4E60; --hazard:#C4432C; --hazard-bg:#F3DCD2; --dirty:#96731F; --dirty-bg:#EFE3C4; --resolved:#4F7A5C; --resolved-bg:#DCE8DD; --line:#C9BC9C; --card:#FBF8EF; --radius:10px; --shadow:0 2px 8px rgba(28,58,74,0.12); } *{box-sizing:border-box;} body{margin:0;background:var(--paper);color:var(--ink);font-family:'Sarabun',sans-serif;} h1,h2,h3,.display{font-family:'Chakra Petch',sans-serif;} button{font-family:inherit;}.page{max-width:1200px;margin:0 auto;padding:20px 16px 60px;}
header.masthead{
border:2px solid var(--ink);
border-radius:var(--radius);
background:var(--card);
padding:18px 22px;
display:flex;
flex-wrap:wrap;
gap:16px;
align-items:center;
justify-content:space-between;
margin-bottom:18px;
}
header.masthead .title-block h1{
margin:0 0 4px;
font-size:1.4rem;
font-weight:700;
letter-spacing:0.2px;
}
header.masthead .title-block p{
margin:0;
color:var(--ink-soft);
font-size:0.92rem;
}
.stat-row{display:flex;gap:10px;flex-wrap:wrap;}
.stat-pill{
display:flex;align-items:center;gap:8px;
border:1.5px solid var(--line);
border-radius:999px;
padding:7px 14px;
background:var(--paper);
font-size:0.88rem;
}
.stat-pill b{font-family:'Chakra Petch',sans-serif;font-size:1.05rem;}
.dot{width:10px;height:10px;border-radius:50%;display:inline-block;flex:none;}
.dot.hazard{background:var(--hazard);}
.dot.dirty{background:var(--dirty);}
.dot.resolved{background:var(--resolved);}
.layout{display:grid;grid-template-columns:1.5fr 1fr;gap:18px;align-items:start;}
@media (max-width:880px){ .layout{grid-template-columns:1fr;} }
.map-panel{
background:var(--blueprint);
border-radius:var(--radius);
padding:16px;
box-shadow:var(--shadow);
position:sticky;
top:16px;
}
@media (max-width:880px){ .map-panel{position:static;} }
.map-head{
display:flex;justify-content:space-between;align-items:baseline;
color:var(--paper);margin-bottom:10px;
}
.map-head h2{margin:0;font-size:1.05rem;font-weight:600;color:var(--paper);}
.map-head span{font-size:0.78rem;color:#AFC4CC;}
.map-wrap{
position:relative;
width:100%;
aspect-ratio:1000/900;
border-radius:6px;
overflow:hidden;
cursor:crosshair;
border:1px solid var(--blueprint-dim);
}
.map-wrap svg{width:100%;height:100%;display:block;}
.map-hint{
color:#AFC4CC;font-size:0.78rem;margin-top:8px;
}
.pin{
position:absolute;
width:18px;height:18px;
border-radius:50%;
transform:translate(-50%,-50%);
border:2px solid var(--paper);
cursor:pointer;
box-shadow:0 1px 4px rgba(0,0,0,0.4);
display:flex;align-items:center;justify-content:center;
padding:0;
animation:drop 0.28s ease-out;
}
.pin.type-hazard{background:var(--hazard);}
.pin.type-dirty{background:var(--dirty);}
.pin.status-resolved{background:var(--resolved);opacity:0.55;}
.pin.dimmed{opacity:0.18;}
.pin.sev-high{width:22px;height:22px;}
.pin.sev-medium{width:18px;height:18px;}
.pin.sev-low{width:14px;height:14px;}
@Keyframes drop{from{transform:translate(-50%,-140%);opacity:0;}to{transform:translate(-50%,-50%);opacity:1;}}
.new-pin-marker{
position:absolute;width:14px;height:14px;border-radius:50%;
background:transparent;border:2px dashed var(--paper);
transform:translate(-50%,-50%);pointer-events:none;
}
.sidebar{display:flex;flex-direction:column;gap:12px;}
.filters{display:flex;gap:8px;flex-wrap:wrap;}
.filter-btn{
border:1.5px solid var(--line);
background:var(--card);
color:var(--ink);
padding:7px 13px;
border-radius:999px;
font-size:0.85rem;
cursor:pointer;
}
.filter-btn.active{background:var(--ink);border-color:var(--ink);color:var(--paper);}
.report-list{display:flex;flex-direction:column;gap:10px;max-height:640px;overflow-y:auto;padding-right:2px;}
.report-card{
background:var(--card);
border:1px solid var(--line);
border-left:5px solid var(--hazard);
border-radius:8px;
padding:12px 14px;
box-shadow:var(--shadow);
}
.report-card.type-dirty{border-left-color:var(--dirty);}
.report-card.status-resolved{border-left-color:var(--resolved);opacity:0.75;}
.report-card .rc-top{display:flex;justify-content:space-between;gap:8px;align-items:flex-start;}
.report-card .rc-zone{font-weight:600;font-size:0.95rem;}
.report-card .rc-time{font-size:0.75rem;color:var(--ink-soft);white-space:nowrap;}
.report-card .rc-detail{font-size:0.88rem;margin:6px 0 8px;line-height:1.5;color:#3A4640;}
.badge{
display:inline-block;font-size:0.72rem;padding:2px 8px;border-radius:999px;
margin-right:6px;font-weight:600;
}
.badge.hazard{background:var(--hazard-bg);color:var(--hazard);}
.badge.dirty{background:var(--dirty-bg);color:var(--dirty);}
.badge.resolved{background:var(--resolved-bg);color:var(--resolved);}
.rc-actions{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;}
.rc-btn{
border:none;background:none;font-size:0.78rem;cursor:pointer;
padding:5px 10px;border-radius:6px;font-weight:600;
}
.rc-btn.resolve{background:var(--resolved-bg);color:var(--resolved);}
.rc-btn.reopen{background:var(--dirty-bg);color:var(--dirty);}
.rc-btn.delete{background:transparent;color:#A15A4A;}
.rc-btn.delete.confirming{background:var(--hazard-bg);color:var(--hazard);}
.empty-state{
border:1.5px dashed var(--line);border-radius:8px;
padding:24px 16px;text-align:center;color:var(--ink-soft);font-size:0.88rem;
}
.admin-row{margin-top:4px;}
.admin-row button{
font-size:0.72rem;color:var(--ink-soft);background:none;border:none;
text-decoration:underline;cursor:pointer;padding:4px 0;
}
.form-overlay{
position:fixed;inset:0;background:rgba(28,48,43,0.35);
display:none;align-items:flex-end;justify-content:center;z-index:40;
}
.form-overlay.open{display:flex;}
@media (min-width:640px){ .form-overlay{align-items:center;} }
.form-panel{
background:var(--card);width:100%;max-width:460px;
border-radius:14px 14px 0 0;
padding:20px 22px 24px;
box-shadow:0 -4px 20px rgba(0,0,0,0.25);
animation:slideup 0.22s ease-out;
max-height:88vh;overflow-y:auto;
}
@media (min-width:640px){ .form-panel{border-radius:14px;} }
@Keyframes slideup{from{transform:translateY(24px);opacity:0;}to{transform:translateY(0);opacity:1;}}
.form-panel h3{margin:0 0 2px;font-size:1.15rem;}
.form-panel .zone-line{color:var(--ink-soft);font-size:0.85rem;margin-bottom:2px;}
.form-panel .zone-note{color:var(--dirty);font-size:0.78rem;margin-bottom:14px;background:var(--dirty-bg);padding:6px 10px;border-radius:6px;}
.type-toggle{display:flex;gap:8px;margin-bottom:14px;}
.type-toggle button{
flex:1;padding:12px 8px;border-radius:8px;border:1.5px solid var(--line);
background:var(--paper);cursor:pointer;font-size:0.88rem;font-weight:600;color:var(--ink-soft);
text-align:center;
}
.type-toggle button.active.hazard{background:var(--hazard-bg);border-color:var(--hazard);color:var(--hazard);}
.type-toggle button.active.dirty{background:var(--dirty-bg);border-color:var(--dirty);color:var(--dirty);}
.sev-row{display:flex;gap:8px;margin-bottom:14px;}
.sev-row button{
flex:1;padding:8px;border-radius:6px;border:1.5px solid var(--line);
background:var(--paper);cursor:pointer;font-size:0.82rem;color:var(--ink-soft);
}
.sev-row button.active{background:var(--ink);border-color:var(--ink);color:var(--paper);}
label{display:block;font-size:0.85rem;font-weight:600;margin-bottom:6px;color:var(--ink);}
textarea,input[type=text]{
width:100%;border:1.5px solid var(--line);border-radius:8px;
padding:10px 12px;font-family:inherit;font-size:0.9rem;margin-bottom:14px;
background:var(--paper);color:var(--ink);
}
textarea{min-height:80px;resize:vertical;}
.row-2{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
.form-actions{display:flex;gap:10px;margin-top:4px;}
.btn{padding:11px 18px;border-radius:8px;border:none;cursor:pointer;font-weight:600;font-size:0.9rem;}
.btn-cancel{background:var(--paper-dim);color:var(--ink);}
.btn-submit{background:var(--ink);color:var(--paper);flex:1;}
.btn-submit:disabled{opacity:0.4;cursor:not-allowed;}
.toast{
position:fixed;bottom:18px;left:50%;transform:translateX(-50%);
background:var(--ink);color:var(--paper);padding:10px 18px;border-radius:8px;
font-size:0.85rem;z-index:60;box-shadow:var(--shadow);
}
.banner-error{
background:var(--hazard-bg);color:var(--hazard);border-radius:8px;padding:10px 14px;
font-size:0.85rem;margin-bottom:14px;
}
.skeleton{color:var(--ink-soft);font-size:0.85rem;padding:14px;text-align:center;}
</style>
แผนผังแจ้งจุดเสี่ยง & จุดสกปรก — ขอนแก่นวิทยายน
คลิกจุดบนแผนผังเพื่อแจ้งเหตุ — ข้อมูลนี้แสดงให้ทุกคนที่เปิดหน้านี้เห็นร่วมกัน
ผังโรงเรียน
คลิกบนแผนผังเพื่อแจ้งจุดใหม่แจ้งจุดใหม่
บริเวณ: —
คลิกที่แผนผังเพื่อแจ้งจุดแรก
แจ้งโดย '+escapeHtml(r.reporter)+'' : ""}