Skip to content

Wat #334

Description

@682-61441-ship-it

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>

แผนผังแจ้งจุดเสี่ยง & จุดสกปรก — ขอนแก่นวิทยายน

คลิกจุดบนแผนผังเพื่อแจ้งเหตุ — ข้อมูลนี้แสดงให้ทุกคนที่เปิดหน้านี้เห็นร่วมกัน

ผังโรงเรียน

คลิกบนแผนผังเพื่อแจ้งจุดใหม่
      <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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions