from pathlib import Path
html = r'''<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#FFD91E" />
<title>Fieldmate — แพลตฟอร์มข้อมูลภาคสนาม</title>
<!-- Leaflet: แผนที่ OpenStreetMap แบบไม่ต้องใช้ API Key -->
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIINfQ3ynh1Z7ZtY1t0zT0kG2x9w4V0aA6M="
crossorigin=""
/>
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>
<style>
:root{
--yellow:#FFD91E;
--yellow-soft:#FFF7C2;
--ink:#241918;
--ink-2:#443A38;
--bg:#F5F6F7;
--card:#FFFFFF;
--line:#E7E3E1;
--muted:#746D6A;
--success:#20865A;
--danger:#C73B32;
--warning:#E67E22;
--info:#2D6CDF;
--shadow:0 12px 32px rgba(36,25,24,.08);
--radius:20px;
--sidebar:250px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
margin:0;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans Thai",Tahoma,sans-serif;
color:var(--ink);
background:var(--bg);
min-height:100vh;
}
button,input,select,textarea{font:inherit}
button{cursor:pointer}
a{color:inherit;text-decoration:none}
.hidden{display:none!important}
.muted{color:var(--muted)}
.tiny{font-size:12px}
.small{font-size:14px}
.stack{display:flex;flex-direction:column;gap:12px}
.row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.spread{display:flex;justify-content:space-between;align-items:center;gap:12px}
.nowrap{white-space:nowrap}
.danger-text{color:var(--danger)}
.success-text{color:var(--success)}
.warning-text{color:var(--warning)}
.info-text{color:var(--info)}
.full{width:100%}
.app-shell{
display:grid;
grid-template-columns:var(--sidebar) 1fr;
min-height:100vh;
}
.sidebar{
position:fixed;
inset:0 auto 0 0;
width:var(--sidebar);
padding:20px 14px;
background:var(--ink);
color:#fff;
z-index:50;
display:flex;
flex-direction:column;
gap:20px;
}
.brand{
display:flex;
gap:12px;
align-items:center;
padding:6px 8px 16px;
border-bottom:1px solid rgba(255,255,255,.12);
}
.brand-mark{
width:42px;height:42px;border-radius:14px;
background:var(--yellow);
color:var(--ink);
display:grid;place-items:center;
font-size:24px;font-weight:900;
box-shadow:inset 0 -4px 0 rgba(0,0,0,.08);
}
.brand h1{font-size:20px;margin:0}
.brand p{margin:2px 0 0;color:#BFB7B5;font-size:12px}
.nav{display:flex;flex-direction:column;gap:5px;overflow:auto}
.nav button{
border:0;background:transparent;color:#DDD6D3;
display:flex;align-items:center;gap:12px;
padding:12px 14px;border-radius:14px;text-align:left;
transition:.2s;
}
.nav button:hover,.nav button.active{
background:var(--yellow);color:var(--ink);font-weight:700;
}
.nav .icon{width:24px;text-align:center;font-size:18px}
.sidebar-footer{
margin-top:auto;
background:rgba(255,255,255,.08);
border:1px solid rgba(255,255,255,.1);
padding:12px;border-radius:16px;
font-size:13px;color:#D8D0CE;
}
main{
grid-column:2;
min-width:0;
padding:24px 28px 100px;
}
.topbar{
position:sticky;top:0;z-index:35;
background:rgba(245,246,247,.88);
backdrop-filter:blur(14px);
display:flex;justify-content:space-between;align-items:center;
padding:8px 0 18px;
margin-bottom:8px;
}
.page-title h2{margin:0;font-size:28px;line-height:1.2}
.page-title p{margin:6px 0 0;color:var(--muted)}
.top-actions{display:flex;align-items:center;gap:10px}
.avatar{
width:42px;height:42px;border-radius:50%;
display:grid;place-items:center;background:var(--ink);color:#fff;font-weight:800;
}
.btn{
border:1px solid var(--line);
background:#fff;color:var(--ink);
border-radius:14px;padding:11px 16px;
display:inline-flex;align-items:center;justify-content:center;
gap:8px;font-weight:700;min-height:44px;
transition:.18s ease;
}
.btn:hover{transform:translateY(-1px);box-shadow:0 6px 14px rgba(36,25,24,.08)}
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none;box-shadow:none}
.btn-primary{background:var(--yellow);border-color:var(--yellow)}
.btn-dark{background:var(--ink);border-color:var(--ink);color:#fff}
.btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
.btn-soft{background:var(--yellow-soft);border-color:#F4ECA5}
.btn-ghost{background:transparent}
.btn-sm{padding:8px 11px;min-height:36px;border-radius:11px;font-size:13px}
.icon-btn{
width:44px;height:44px;border-radius:14px;
border:1px solid var(--line);background:#fff;
display:grid;place-items:center;
}
.page{display:none;animation:fade .22s ease}
.page.active{display:block}
@keyframes fade{from{opacity:.3;transform:translateY(4px)}to{opacity:1;transform:none}}
.grid{display:grid;gap:16px}
.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}
.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}
.grid-6{grid-template-columns:repeat(6,minmax(0,1fr))}
.dashboard-grid{grid-template-columns:1.4fr .8fr}
.content-sidebar{grid-template-columns:minmax(0,1fr) 380px}
.card{
background:var(--card);
border:1px solid var(--line);
border-radius:var(--radius);
box-shadow:var(--shadow);
padding:18px;
min-width:0;
}
.card.flat{box-shadow:none}
.card h3{margin:0 0 4px;font-size:18px}
.card-title{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:14px}
.hero{
background:var(--ink);
color:#fff;
padding:26px;
border-radius:26px;
position:relative;
overflow:hidden;
}
.hero:after{
content:"";
position:absolute;right:-40px;top:-70px;
width:220px;height:220px;border-radius:50%;
background:var(--yellow);opacity:.16;
}
.hero h2{font-size:28px;margin:0 0 8px}
.hero p{color:#D7CFCD;max-width:650px;margin:0 0 20px}
.hero .btn{position:relative;z-index:2}
.stat-card{padding:16px;box-shadow:none}
.stat-card .label{font-size:13px;color:var(--muted)}
.stat-card .value{font-size:28px;font-weight:850;margin:7px 0 3px}
.stat-card .delta{font-size:12px}
.stat-card.warning{border-left:5px solid var(--warning)}
.stat-card.success{border-left:5px solid var(--success)}
.stat-card.info{border-left:5px solid var(--info)}
.stat-card.yellow{border-left:5px solid var(--yellow)}
.quick-actions{
display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;
}
.quick{
border:1px solid var(--line);
background:#fff;border-radius:16px;
padding:15px;text-align:left;
min-height:90px;
}
.quick:hover{border-color:var(--yellow);background:#FFFDF2}
.quick .qicon{font-size:24px;margin-bottom:10px}
.quick strong{display:block}
.quick span{color:var(--muted);font-size:12px}
.map{
width:100%;
min-height:460px;
border-radius:18px;
overflow:hidden;
background:
linear-gradient(135deg,#EBECEE 25%,transparent 25%) -20px 0/40px 40px,
linear-gradient(225deg,#EBECEE 25%,transparent 25%) -20px 0/40px 40px,
linear-gradient(315deg,#EBECEE 25%,transparent 25%) 0 0/40px 40px,
linear-gradient(45deg,#EBECEE 25%,#F7F7F7 25%) 0 0/40px 40px;
}
.map.small{min-height:300px}
.map-fallback{
min-height:300px;display:grid;place-items:center;text-align:center;padding:24px;color:var(--muted);
}
.toolbar{
display:flex;gap:10px;flex-wrap:wrap;margin-bottom:14px;
}
.input,.select,.textarea{
width:100%;border:1px solid var(--line);background:#fff;
border-radius:13px;padding:12px 13px;color:var(--ink);
outline:none;min-height:44px;
}
.input:focus,.select:focus,.textarea:focus{border-color:#B6A100;box-shadow:0 0 0 3px rgba(255,217,30,.2)}
.textarea{min-height:100px;resize:vertical}
.field label{display:block;font-size:13px;font-weight:700;margin:0 0 7px}
.field small{display:block;color:var(--muted);margin-top:5px}
.field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.field-grid-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.searchbox{position:relative;flex:1;min-width:240px}
.searchbox input{padding-left:42px}
.searchbox:before{content:"⌕";position:absolute;left:15px;top:10px;font-size:22px;color:var(--muted)}
.chip{
display:inline-flex;align-items:center;gap:6px;
border-radius:999px;padding:6px 10px;font-size:12px;font-weight:700;
background:#F0EFEE;color:var(--ink-2);
}
.chip.green{background:#E6F5EE;color:var(--success)}
.chip.red{background:#FBE9E7;color:var(--danger)}
.chip.orange{background:#FFF0DF;color:#A75400}
.chip.blue{background:#E9F0FF;color:var(--info)}
.chip.yellow{background:var(--yellow-soft);color:#6B5E00}
.list{display:flex;flex-direction:column;gap:10px}
.list-item{
border:1px solid var(--line);border-radius:16px;padding:14px;
background:#fff;
}
.list-item:hover{border-color:#D8CB66}
.record-card{
display:grid;grid-template-columns:88px 1fr auto;gap:14px;align-items:center;
}
.thumb{
width:88px;height:72px;border-radius:12px;object-fit:cover;background:#ECE8E6;
}
.record-card h4{margin:0 0 5px}
.record-card p{margin:3px 0;color:var(--muted);font-size:13px}
.meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
table{width:100%;border-collapse:collapse}
th,td{text-align:left;padding:13px 10px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:top}
th{color:var(--muted);font-size:12px;text-transform:none}
tr:hover td{background:#FFFDF3}
.stepper{display:flex;gap:8px;overflow:auto;padding:2px 2px 14px}
.step{
min-width:135px;padding:10px 12px;border-radius:12px;border:1px solid var(--line);
background:#fff;color:var(--muted);font-size:12px;font-weight:700;
}
.step.active{background:var(--yellow);color:var(--ink);border-color:var(--yellow)}
.step.done{background:#EAF7F0;color:var(--success);border-color:#BFE3D0}
.step-panel{display:none}
.step-panel.active{display:block}
.step-actions{display:flex;justify-content:space-between;gap:10px;margin-top:20px}
.camera-box{
background:#121212;border-radius:18px;overflow:hidden;min-height:320px;
position:relative;display:grid;place-items:center;color:#fff;
}
video{width:100%;max-height:500px;object-fit:cover}
.camera-active{
position:absolute;top:12px;left:12px;
background:rgba(199,59,50,.92);padding:7px 10px;border-radius:999px;font-size:12px;
}
.photo-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.photo-item{position:relative;border-radius:14px;overflow:hidden;border:1px solid var(--line);background:#eee}
.photo-item img{width:100%;height:110px;object-fit:cover;display:block}
.photo-item button{position:absolute;top:6px;right:6px;border:0;background:rgba(0,0,0,.65);color:#fff;border-radius:50%;width:28px;height:28px}
.photo-item .cover{position:absolute;bottom:6px;left:6px;background:var(--yellow);padding:4px 7px;border-radius:999px;font-size:11px;font-weight:800}
.warning-card{border-left:5px solid var(--warning)}
.warning-card.high{border-left-color:var(--danger)}
.warning-card.normal{border-left-color:var(--info)}
.progress{height:8px;border-radius:999px;background:#E9E7E5;overflow:hidden}
.progress span{display:block;height:100%;background:var(--yellow)}
.storage-visual{
border-radius:24px;
padding:24px;
background:linear-gradient(180deg,#2E2523,#241918);
color:#fff;position:relative;overflow:hidden;
}
.storage-visual:after{
content:"";
position:absolute;inset:auto -20px -60px auto;
width:200px;height:200px;border-radius:50%;
background:var(--yellow);opacity:.13;
}
.storage-meter{
height:18px;border-radius:999px;background:rgba(255,255,255,.14);overflow:hidden;margin:16px 0 8px
}
.storage-meter span{display:block;height:100%;width:63%;background:var(--yellow)}
.folder-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.folder{
border:1px solid var(--line);background:#fff;border-radius:17px;padding:16px;
}
.folder .folder-icon{font-size:28px}
.folder strong{display:block;margin:8px 0 4px}
.folder small{color:var(--muted)}
.bottom-nav{
display:none;
position:fixed;bottom:0;left:0;right:0;z-index:60;
background:#fff;border-top:1px solid var(--line);
padding:8px 8px calc(8px + env(safe-area-inset-bottom));
grid-template-columns:repeat(5,1fr);
box-shadow:0 -8px 24px rgba(36,25,24,.08);
}
.bottom-nav button{
border:0;background:transparent;color:var(--muted);
font-size:11px;display:flex;flex-direction:column;align-items:center;gap:4px;
padding:5px;
}
.bottom-nav button.active{color:var(--ink);font-weight:800}
.bottom-nav .add-mobile{
width:52px;height:52px;border-radius:18px;background:var(--yellow);
color:var(--ink);font-size:24px;margin-top:-22px;box-shadow:0 8px 18px rgba(0,0,0,.18);
}
.modal{
position:fixed;inset:0;background:rgba(24,18,17,.62);
z-index:100;display:none;align-items:center;justify-content:center;padding:18px;
}
.modal.open{display:flex}
.modal-card{
width:min(720px,100%);max-height:92vh;overflow:auto;
background:#fff;border-radius:24px;padding:20px;box-shadow:0 30px 80px rgba(0,0,0,.28);
}
.modal-card.wide{width:min(1000px,100%)}
.modal-header{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:16px}
.modal-header h3{margin:0}
.toast{
position:fixed;right:22px;bottom:24px;z-index:200;
padding:13px 16px;background:var(--ink);color:#fff;
border-radius:14px;box-shadow:var(--shadow);display:none;max-width:360px;
}
.toast.show{display:block;animation:toastIn .2s ease}
@keyframes toastIn{from{transform:translateY(14px);opacity:0}to{transform:none;opacity:1}}
.offline-banner{
display:none;background:#FFF0DF;color:#884600;border:1px solid #F3C89B;
padding:10px 14px;border-radius:14px;margin-bottom:14px;
}
.offline-banner.show{display:block}
.alert{
padding:13px 14px;border-radius:14px;border:1px solid;
background:#fff;margin:10px 0;
}
.alert.warning{border-color:#F0C48B;background:#FFF8EF}
.alert.danger{border-color:#EDB5B0;background:#FFF2F1}
.alert.success{border-color:#BCE0CD;background:#EFFAF4}
.alert.info{border-color:#BFD0F3;background:#F3F7FF}
@media(max-width:1180px){
.grid-6{grid-template-columns:repeat(3,1fr)}
.grid-4{grid-template-columns:repeat(2,1fr)}
.dashboard-grid,.content-sidebar{grid-template-columns:1fr}
.folder-grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:820px){
:root{--sidebar:0px}
.app-shell{display:block}
.sidebar{display:none}
main{padding:16px 14px 105px}
.topbar{padding:8px 0 14px}
.topbar .page-title p{display:none}
.top-actions .btn{display:none}
.page-title h2{font-size:22px}
.bottom-nav{display:grid}
.grid-2,.grid-3,.grid-4,.grid-6{grid-template-columns:1fr}
.quick-actions{grid-template-columns:repeat(2,1fr)}
.field-grid,.field-grid-3{grid-template-columns:1fr}
.record-card{grid-template-columns:72px 1fr}
.record-card .record-actions{grid-column:1/-1;display:flex;gap:8px}
.thumb{width:72px;height:64px}
.photo-grid{grid-template-columns:repeat(2,1fr)}
.folder-grid{grid-template-columns:repeat(2,1fr)}
.hero{padding:22px}
.hero h2{font-size:24px}
table{display:block;overflow:auto}
.map{min-height:380px}
}
@media(max-width:470px){
.quick-actions{grid-template-columns:1fr 1fr}
.folder-grid{grid-template-columns:1fr 1fr}
.card{padding:14px;border-radius:17px}
.btn{padding:10px 12px}
}
</style>
</head>
<body>
<div class="app-shell">
<aside class="sidebar">
<div class="brand">
<div class="brand-mark">F</div>
<div>
<h1>Fieldmate</h1>
<p>Central Field Data Platform</p>
</div>
</div>
<nav class="nav" id="desktopNav">
<button class="active" data-page="dashboard"><span class="icon">⌂</span>ภาพรวม</button>
<button data-page="map"><span class="icon">⌖</span>แผนที่ข้อมูล</button>
<button data-page="search"><span class="icon">⌕</span>ค้นหาข้อมูล</button>
<button data-page="add"><span class="icon">+</span>เพิ่มข้อมูล</button>
<button data-page="storage"><span class="icon">▣</span>คลังข้อมูลกลาง</button>
<button data-page="team"><span class="icon">♙</span>งานของทีม</button>
<button data-page="safety"><span class="icon">⚠</span>แจ้งเตือนพื้นที่</button>
<button data-page="nearby"><span class="icon">☕</span>พักไหนดีใกล้ฉัน</button>
<button data-page="reports"><span class="icon">▥</span>รายงาน</button>
</nav>
<div class="sidebar-footer">
<strong>สถานะระบบ</strong>
<div class="row" style="margin-top:8px">
<span style="width:9px;height:9px;border-radius:50%;background:#5DD39E"></span>
<span id="syncStatusSidebar">เชื่อมต่อและ Sync แล้ว</span>
</div>
</div>
</aside>
<main>
<div id="offlineBanner" class="offline-banner">
คุณกำลังใช้งานแบบ Offline ข้อมูลใหม่จะถูกเก็บในอุปกรณ์และ Sync เมื่อกลับมาออนไลน์
</div>
<header class="topbar">
<div class="page-title">
<h2 id="topPageTitle">ภาพรวม</h2>
<p id="topPageSubtitle">ข้อมูลภาคสนามของทีมในที่เดียว</p>
</div>
<div class="top-actions">
<button class="btn btn-primary" onclick="openPage('add')">+ เพิ่มข้อมูลภาคสนาม</button>
<button class="icon-btn" title="แจ้งเตือน">🔔</button>
<div class="avatar">ปอ</div>
</div>
</header>
<!-- DASHBOARD -->
<section class="page active" id="page-dashboard">
<div class="hero">
<h2>สวัสดี คุณปอ 👋</h2>
<p id="todayText">พร้อมออกสำรวจวันนี้หรือยัง ตรวจสอบข้อมูลเดิม จุดที่ต้องระวัง และงานที่ได้รับมอบหมายก่อนเดินทาง</p>
<div class="row">
<button class="btn btn-primary" onclick="openPage('add')">+ เพิ่มข้อมูลภาคสนาม</button>
<button class="btn btn-ghost" style="color:#fff;border-color:rgba(255,255,255,.25)" onclick="requestLocationWithExplanation()">⌖ ใช้ตำแหน่งปัจจุบัน</button>
</div>
</div>
<div class="grid grid-6" style="margin-top:16px">
<div class="card stat-card yellow"><div class="label">ข้อมูลภาคสนามทั้งหมด</div><div class="value" id="statAll">248</div><div class="delta">รายการในคลังกลาง</div></div>
<div class="card stat-card success"><div class="label">เพิ่มใหม่เดือนนี้</div><div class="value">32</div><div class="delta success-text">+12% จากเดือนก่อน</div></div>
<div class="card stat-card warning"><div class="label">ข้อมูลที่อาจซ้ำ</div><div class="value">6</div><div class="delta warning-text">รอตรวจสอบ</div></div>
<div class="card stat-card info"><div class="label">ควรสำรวจซ้ำ</div><div class="value">18</div><div class="delta info-text">เกิน 6 เดือน</div></div>
<div class="card stat-card warning"><div class="label">จุดที่ต้องระวัง</div><div class="value">12</div><div class="delta danger-text">3 จุดเสี่ยงสูง</div></div>
<div class="card stat-card success"><div class="label">ป้องกันการลงพื้นที่ซ้ำ</div><div class="value">24</div><div class="delta success-text">ครั้ง</div></div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title"><div><h3>ทางลัดสำหรับงานภาคสนาม</h3><div class="muted small">กดใช้งานได้ทันที</div></div></div>
<div class="quick-actions">
<button class="quick" onclick="openCameraPermissionModal()"><div class="qicon">📷</div><strong>ถ่ายรูปป้าย</strong><span>เปิดกล้องของอุปกรณ์</span></button>
<button class="quick" onclick="requestLocationWithExplanation()"><div class="qicon">📍</div><strong>ใช้ตำแหน่งปัจจุบัน</strong><span>ค้นหาข้อมูลรอบตัว</span></button>
<button class="quick" onclick="searchNearMe()"><div class="qicon">🔎</div><strong>ค้นหาข้อมูลใกล้ฉัน</strong><span>ภายใน 250 เมตร</span></button>
<button class="quick" onclick="openPage('safety')"><div class="qicon">⚠️</div><strong>ดูจุดที่ต้องระวัง</strong><span>ตรวจสอบก่อนเดินทาง</span></button>
<button class="quick" onclick="openPage('team')"><div class="qicon">✅</div><strong>ดูงานที่ได้รับมอบหมาย</strong><span>3 งานวันนี้</span></button>
<button class="quick" onclick="openPage('nearby')"><div class="qicon">🍜</div><strong>ดูร้านอาหารใกล้เคียง</strong><span>สำหรับพักระหว่างงาน</span></button>
</div>
</div>
<div class="grid dashboard-grid" style="margin-top:16px">
<div class="card">
<div class="card-title">
<div><h3>ข้อมูลรอบพื้นที่</h3><div class="muted small" id="locationSummary">ยังไม่ได้ใช้ตำแหน่งปัจจุบัน</div></div>
<button class="btn btn-sm" onclick="openPage('map')">เปิด Smart Map</button>
</div>
<div id="dashboardMap" class="map small"></div>
</div>
<div class="stack">
<div class="card">
<div class="card-title"><h3>งานที่ครบกำหนดวันนี้</h3><span class="chip red">3 งาน</span></div>
<div class="list" id="dashboardTasks"></div>
</div>
<div class="card">
<div class="card-title"><h3>จุดเตือนล่าสุด</h3><button class="btn btn-sm" onclick="openPage('safety')">ดูทั้งหมด</button></div>
<div class="list" id="dashboardWarnings"></div>
</div>
</div>
</div>
<div class="grid grid-2" style="margin-top:16px">
<div class="card">
<div class="card-title"><h3>กิจกรรมล่าสุดของทีม</h3></div>
<div class="list" id="teamActivity"></div>
</div>
<div class="card">
<div class="card-title">
<div><h3>คลังข้อมูลกลาง</h3><div class="muted small">สถานะพื้นที่จัดเก็บและ Sync</div></div>
<button class="btn btn-sm btn-soft" onclick="openPage('storage')">เปิดคลัง</button>
</div>
<div class="spread"><span>พื้นที่ใช้งาน</span><strong>63%</strong></div>
<div class="progress" style="margin:10px 0 14px"><span style="width:63%"></span></div>
<div class="grid grid-3">
<div><strong>2,184</strong><div class="tiny muted">รูปภาพ</div></div>
<div><strong>164</strong><div class="tiny muted">เอกสาร</div></div>
<div><strong id="pendingSyncCount">0</strong><div class="tiny muted">รอ Sync</div></div>
</div>
</div>
</div>
</section>
<!-- MAP -->
<section class="page" id="page-map">
<div class="card">
<div class="toolbar">
<div class="searchbox"><input id="mapSearchInput" class="input" placeholder="ค้นหาสถานที่ เลขที่โฉนด เบอร์โทร ชื่อโครงการ หรือรหัสรายการ"></div>
<button class="btn btn-primary" onclick="filterMapRecords()">ค้นหา</button>
<button class="btn" onclick="recenterMap()">⌖ ตำแหน่งฉัน</button>
<button class="btn" id="trackingBtn" onclick="toggleTracking()">▶ เริ่มติดตาม</button>
<button class="btn" onclick="fitAllMarkers()">ขยายให้เห็นทั้งหมด</button>
</div>
<div class="grid grid-4" style="margin-bottom:14px">
<select class="select" id="mapPropertyFilter" onchange="filterMapRecords()">
<option value="">ทุกประเภททรัพย์</option>
<option>บ้านเดี่ยว</option><option>ทาวน์โฮม</option><option>คอนโดมิเนียม</option>
<option>ที่ดินเปล่า</option><option>อาคารพาณิชย์</option><option>โรงงาน</option>
</select>
<select class="select" id="mapStatusFilter" onchange="filterMapRecords()">
<option value="">ทุกสถานะ</option>
<option>ข้อมูลใหม่</option><option>ยืนยันแล้ว</option><option>ควรสำรวจซ้ำ</option><option>อาจซ้ำ</option><option>ฉบับร่าง</option>
</select>
<select class="select" id="mapWarningFilter" onchange="filterMapRecords()">
<option value="">จุดเตือนทุกประเภท</option>
<option>สุนัขดุ</option><option>ทางเข้าแคบ</option><option>น้ำท่วม</option><option>ถนนชำรุด</option>
</select>
<select class="select" id="mapPlaceFilter" onchange="renderMapMarkers()">
<option value="">ไม่แสดงสถานที่ใกล้เคียง</option>
<option>ร้านอาหาร</option><option>ร้านกาแฟ</option><option>ร้านสะดวกซื้อ</option>
<option>ห้องน้ำ</option><option>ปั๊มน้ำมัน</option><option>ATM</option>
</select>
</div>
<div class="row" style="margin-bottom:10px">
<span class="chip blue" id="gpsChip">GPS: ยังไม่เชื่อมต่อ</span>
<span class="chip" id="accuracyChip">ความแม่นยำ: —</span>
<span class="chip" id="updateChip">อัปเดตล่าสุด: —</span>
</div>
<div class="grid content-sidebar">
<div id="mainMap" class="map"></div>
<aside class="card flat" id="mapSidePanel">
<h3>เลือกหมุดบนแผนที่</h3>
<p class="muted">คลิกหมุดข้อมูล จุดเตือน หรือสถานที่ใกล้เคียงเพื่อดูรายละเอียด</p>
</aside>
</div>
</div>
</section>
<!-- SEARCH -->
<section class="page" id="page-search">
<div class="card">
<div class="toolbar">
<div class="searchbox"><input id="centralSearchInput" class="input" placeholder="ค้นหาที่อยู่ รหัสรายการ โฉนด เบอร์โทร โครงการ เจ้าหน้าที่ หรือหมายเหตุ" oninput="renderSearchResults()"></div>
<select class="select" style="max-width:200px" id="searchSort" onchange="renderSearchResults()">
<option value="latest">ล่าสุด</option>
<option value="nearest">ใกล้ที่สุด</option>
<option value="oldest">อัปเดตเก่าสุด</option>
</select>
<button class="btn" onclick="exportRecords()">⇩ Export CSV</button>
</div>
<div class="grid grid-4" style="margin-bottom:15px">
<select class="select" id="searchType" onchange="renderSearchResults()">
<option value="">ทุกประเภททรัพย์</option>
<option>บ้านเดี่ยว</option><option>ทาวน์โฮม</option><option>คอนโดมิเนียม</option><option>ที่ดินเปล่า</option>
</select>
<select class="select" id="searchArea" onchange="renderSearchResults()">
<option value="">ทุกพื้นที่</option>
<option>บางนา</option><option>ศรีนครินทร์</option><option>ลาดกระบัง</option><option>บางพลี</option><option>สมุทรปราการ</option><option>พระโขนง</option>
</select>
<select class="select" id="searchStatus" onchange="renderSearchResults()">
<option value="">ทุกสถานะ</option>
<option>ข้อมูลใหม่</option><option>ยืนยันแล้ว</option><option>ควรสำรวจซ้ำ</option><option>อาจซ้ำ</option><option>ฉบับร่าง</option>
</select>
<select class="select" id="searchOfficer" onchange="renderSearchResults()">
<option value="">ผู้บันทึกทุกคน</option>
<option>คุณปอ</option><option>คุณเมย์</option><option>คุณต้น</option><option>คุณฝน</option>
</select>
</div>
<div class="spread" style="margin-bottom:12px">
<div><strong id="searchResultCount">0</strong> รายการ</div>
<div class="muted small">แสดงเบอร์โทรแบบปกปิดในหน้ารายการ</div>
</div>
<div id="searchLoading" class="alert info hidden">กำลังค้นหาข้อมูล...</div>
<div id="searchResults" class="list"></div>
<div class="row" style="justify-content:center;margin-top:18px">
<button class="btn btn-sm" onclick="changePage(-1)">ก่อนหน้า</button>
<span class="chip" id="paginationLabel">หน้า 1</span>
<button class="btn btn-sm" onclick="changePage(1)">ถัดไป</button>
</div>
</div>
</section>
<!-- ADD RECORD -->
<section class="page" id="page-add">
<div class="stepper" id="stepper">
<button class="step active" data-step="1">1 ตรวจสอบข้อมูลเดิม</button>
<button class="step" data-step="2">2 ตำแหน่ง</button>
<button class="step" data-step="3">3 กล้องและรูป</button>
<button class="step" data-step="4">4 ข้อมูลทรัพย์</button>
<button class="step" data-step="5">5 Field Safety</button>
<button class="step" data-step="6">6 ตรวจข้อมูลซ้ำ</button>
<button class="step" data-step="7">7 ตรวจสอบและบันทึก</button>
</div>
<div class="card">
<div class="step-panel active" data-step-panel="1">
<div class="card-title"><div><h3>ตรวจสอบข้อมูลเดิมก่อนลงพื้นที่</h3><div class="muted small">ลดการลงพื้นที่ซ้ำและนำข้อมูลเดิมกลับมาใช้</div></div></div>
<div class="grid grid-3">
<div class="field"><label>ระยะค้นหา</label><select id="nearbyDistance" class="select"><option value="50">50 เมตร</option><option value="100">100 เมตร</option><option value="250" selected>250 เมตร</option><option value="500">500 เมตร</option></select></div>
<div class="field"><label>ค้นหาจากข้อมูล</label><input id="duplicateSearchTerm" class="input" placeholder="เบอร์โทร โฉนด ที่อยู่ หรือชื่อโครงการ"></div>
<div class="field"><label> </label><button class="btn btn-primary full" onclick="checkExistingRecords()">ค้นหาข้อมูลเดิม</button></div>
</div>
<div id="existingCheckInfo" class="alert info">ใช้ตำแหน่งปัจจุบันหรือกรอกข้อมูลเพื่อค้นหารายการเดิม</div>
<div id="existingRecordResults" class="list"></div>
<div class="step-actions"><span></span><button class="btn btn-primary" onclick="nextStep()">ยืนยันว่าเป็นจุดใหม่ →</button></div>
</div>
<div class="step-panel" data-step-panel="2">
<div class="card-title"><div><h3>ตำแหน่งสำรวจ</h3><div class="muted small">ตำแหน่งจะถูกบันทึกเมื่อผู้ใช้กดยืนยันเท่านั้น</div></div></div>
<div class="toolbar">
<button class="btn btn-primary" onclick="requestLocationWithExplanation()">⌖ ใช้ตำแหน่งปัจจุบัน</button>
<button class="btn" onclick="toggleTracking()">▶ เริ่มติดตามตำแหน่ง</button>
<button class="btn" onclick="enableManualPin()">📌 ปักหมุดด้วยตนเอง</button>
</div>
<div class="grid grid-3">
<div class="field"><label>Latitude</label><input id="formLat" class="input" type="number" step="any" value="13.6678"></div>
<div class="field"><label>Longitude</label><input id="formLng" class="input" type="number" step="any" value="100.6351"></div>
<div class="field"><label>ความแม่นยำ GPS</label><input id="formAccuracy" class="input" value="ยังไม่มีข้อมูล" readonly></div>
</div>
<div id="formMap" class="map small" style="margin-top:14px"></div>
<div class="field-grid" style="margin-top:14px">
<div class="field"><label>ที่อยู่</label><input id="formAddress" class="input" placeholder="เลขที่ ซอย ถนน"></div>
<div class="field"><label>จุดสังเกต</label><input id="formLandmark" class="input" placeholder="เช่น ตรงข้ามปั๊มน้ำมัน"></div>
<div class="field"><label>จังหวัด</label><input id="formProvince" class="input" value="กรุงเทพมหานคร"></div>
<div class="field"><label>เขต/อำเภอ</label><input id="formDistrict" class="input" value="บางนา"></div>
<div class="field"><label>แขวง/ตำบล</label><input id="formSubdistrict" class="input" value="บางนาเหนือ"></div>
<div class="field"><label>ถนน</label><input id="formRoad" class="input" placeholder="ชื่อถนน"></div>
</div>
<div class="field" style="margin-top:14px"><label>หมายเหตุตำแหน่ง</label><textarea id="formLocationNote" class="textarea" placeholder="รายละเอียดทางเข้า จุดสังเกต หรือวิธีเข้าพื้นที่"></textarea></div>
<div class="step-actions"><button class="btn" onclick="prevStep()">← ย้อนกลับ</button><button class="btn btn-primary" onclick="nextStep()">ใช้ตำแหน่งนี้ →</button></div>
</div>
<div class="step-panel" data-step-panel="3">
<div class="card-title"><div><h3>กล้องและรูปภาพ</h3><div class="muted small">บันทึกป้ายประกาศ สภาพทรัพย์ ทางเข้า ถนน และจุดเสี่ยง</div></div></div>
<div class="toolbar">
<button class="btn btn-primary" onclick="openCameraPermissionModal()">📷 เปิดกล้อง</button>
<label class="btn">🖼 อัปโหลดจากเครื่อง<input id="galleryInput" type="file" accept="image/*" capture="environment" multiple hidden onchange="handleGalleryUpload(event)"></label>
<select id="photoCategory" class="select" style="max-width:220px">
<option>ป้ายประกาศ</option><option>ด้านหน้าทรัพย์</option><option>ทางเข้า</option><option>ถนน</option>
<option>สภาพแวดล้อม</option><option>จุดจอดรถ</option><option>จุดที่ต้องระวัง</option><option>เอกสาร</option><option>อื่น ๆ</option>
</select>
</div>
<div id="photoGrid" class="photo-grid"></div>
<div class="card flat" style="margin-top:18px;background:#FAFAFA">
<div class="card-title"><div><h3>OCR จากป้าย — Prototype</h3><div class="muted small">ข้อมูลตัวอย่าง ต้องตรวจสอบและยืนยันก่อนบันทึก</div></div><button class="btn btn-sm" onclick="runMockOCR()">อ่านข้อมูลจากภาพ</button></div>
<div id="ocrFields" class="field-grid hidden">
<div class="field"><label>เบอร์โทร <span class="chip yellow">อ่านจากภาพ</span></label><input id="ocrPhone" class="input"></div>
<div class="field"><label>ราคาที่ประกาศ <span class="chip yellow">อ่านจากภาพ</span></label><input id="ocrPrice" class="input"></div>
<div class="field"><label>พื้นที่ <span class="chip yellow">อ่านจากภาพ</span></label><input id="ocrArea" class="input"></div>
<div class="field"><label>ชื่อผู้ติดต่อ <span class="chip yellow">อ่านจากภาพ</span></label><input id="ocrContact" class="input"></div>
</div>
</div>
<div class="step-actions"><button class="btn" onclick="prevStep()">← ย้อนกลับ</button><button class="btn btn-primary" onclick="nextStep()">ถัดไป →</button></div>
</div>
<div class="step-panel" data-step-panel="4">
<div class="card-title"><div><h3>ข้อมูลทรัพย์และข้อมูลภาคสนาม</h3><div class="muted small">ไม่มีการวิเคราะห์หรือประเมินราคาตลาด</div></div></div>
<div class="field-grid">
<div class="field"><label>ประเภททรัพย์</label><select id="propertyType" class="select"><option>ที่ดินเปล่า</option><option>บ้านเดี่ยว</option><option>ทาวน์โฮม</option><option>อาคารพาณิชย์</option><option>คอนโดมิเนียม</option><option>โรงงาน</option><option>โกดัง</option><option>อาคารสำนักงาน</option><option>อื่น ๆ</option></select></div>
<div class="field"><label>เลขที่โฉนด</label><input id="titleDeed" class="input"></div>
<div class="field"><label>ชื่อโครงการ</label><input id="projectName" class="input"></div>
<div class="field"><label>พื้นที่ดิน</label><input id="landArea" class="input" placeholder="เช่น 52 ตร.ว."></div>
<div class="field"><label>พื้นที่ใช้สอย</label><input id="usableArea" class="input" placeholder="เช่น 180 ตร.ม."></div>
<div class="field"><label>จำนวนชั้น</label><input id="floors" class="input" type="number"></div>
<div class="field"><label>ห้องนอน</label><input id="bedrooms" class="input" type="number"></div>
<div class="field"><label>ห้องน้ำ</label><input id="bathrooms" class="input" type="number"></div>
<div class="field"><label>ราคาที่ประกาศ (ถ้ามี)</label><input id="advertisedPrice" class="input" placeholder="บันทึกตามป้ายหรือผู้ติดต่อเท่านั้น"></div>
<div class="field"><label>ชื่อผู้ติดต่อ</label><input id="contactName" class="input"></div>
<div class="field"><label>เบอร์โทร</label><input id="contactPhone" class="input"></div>
<div class="field"><label>วันที่พบข้อมูล</label><input id="foundDate" class="input" type="date"></div>
<div class="field"><label>สภาพทรัพย์</label><select id="condition" class="select"><option>สภาพดี</option><option>ใช้งานได้</option><option>ต้องปรับปรุง</option><option>อยู่ระหว่างก่อสร้าง</option><option>ไม่สามารถเข้าตรวจสอบได้</option></select></div>
<div class="field"><label>สภาพการเข้าถึง</label><select id="accessCondition" class="select"><option>รถยนต์เข้าได้</option><option>ทางเข้าแคบ</option><option>ต้องเดินเข้า</option><option>รถเข้าไม่ได้</option></select></div>
<div class="field"><label>สภาพถนน</label><select id="roadCondition" class="select"><option>ถนนลาดยาง</option><option>ถนนคอนกรีต</option><option>ถนนลูกรัง</option><option>ถนนชำรุด</option></select></div>
<div class="field"><label>ที่จอดรถ</label><select id="parking" class="select"><option>มีที่จอด</option><option>จอดริมถนน</option><option>จำกัด</option><option>ไม่มีที่จอด</option></select></div>
<div class="field"><label>สัญญาณโทรศัพท์</label><select id="mobileSignal" class="select"><option>ดี</option><option>ปานกลาง</option><option>อ่อน</option><option>ไม่มีสัญญาณ</option></select></div>
</div>
<div class="field" style="margin-top:14px"><label>หมายเหตุทั่วไป</label><textarea id="generalNotes" class="textarea"></textarea></div>
<div class="step-actions"><button class="btn" onclick="prevStep()">← ย้อนกลับ</button><button class="btn btn-primary" onclick="nextStep()">ถัดไป →</button></div>
</div>
<div class="step-panel" data-step-panel="5">
<div class="card-title"><div><h3>Field Safety</h3><div class="muted small">บันทึกจุดเสี่ยงและข้อควรระวังให้ทีม</div></div></div>
<div class="row" id="quickWarnings">
<button class="btn btn-sm" onclick="setWarning('สุนัขดุ')">🐕 พบสุนัขดุ</button>
<button class="btn btn-sm" onclick="setWarning('ทางเข้าแคบ')">↔ ทางเข้าแคบ</button>
<button class="btn btn-sm" onclick="setWarning('ถนนชำรุด')">🚧 ถนนชำรุด</button>
<button class="btn btn-sm" onclick="setWarning('รถเข้าไม่ได้')">🚫 รถเข้าไม่ได้</button>
<button class="btn btn-sm" onclick="setWarning('น้ำท่วม')">🌊 น้ำท่วม</button>
<button class="btn btn-sm" onclick="setWarning('พื้นที่เปลี่ยว')">🌙 พื้นที่เปลี่ยว</button>
<button class="btn btn-sm" onclick="setWarning('ไม่มีสัญญาณ')">📵 ไม่มีสัญญาณ</button>
<button class="btn btn-sm" onclick="setWarning('ไม่มีที่จอดรถ')">🅿️ ไม่มีที่จอดรถ</button>
</div>
<div class="field-grid" style="margin-top:16px">
<div class="field"><label>ประเภทจุดเตือน</label><select id="warningType" class="select" onchange="toggleDogFields()"><option value="">ไม่มีจุดเตือน</option><option>สุนัขดุ</option><option>พื้นที่เปลี่ยว</option><option>ทางเข้าแคบ</option><option>ถนนชำรุด</option><option>ถนนลูกรัง</option><option>รถยนต์เข้าไม่ได้</option><option>น้ำท่วม</option><option>พื้นที่ก่อสร้าง</option><option>เสี่ยงอุบัติเหตุ</option><option>ไม่มีสัญญาณโทรศัพท์</option><option>ที่จอดรถจำกัด</option><option>พื้นที่ห้ามเข้า</option><option>อื่น ๆ</option></select></div>
<div class="field"><label>ระดับความรุนแรง</label><select id="warningSeverity" class="select"><option>ข้อมูลทั่วไป</option><option>ควรระวัง</option><option>เสี่ยงสูง</option></select></div>
<div class="field"><label>ใช้งานถึงวันที่</label><input id="warningActiveUntil" class="input" type="date"></div>
<div class="field"><label>การมองเห็น</label><select id="warningVisibility" class="select"><option>เห็นได้ทั้งทีม</option><option>เฉพาะทีมของฉัน</option></select></div>
</div>
<div class="field" style="margin-top:14px"><label>รายละเอียด</label><textarea id="warningDescription" class="textarea" placeholder="อธิบายจุดที่พบ เวลา และคำแนะนำ"></textarea></div>
<div id="dogFields" class="card flat hidden" style="margin-top:14px;background:#FFF8EF">
<h3>รายละเอียดสุนัขดุ</h3>
<div class="field-grid" style="margin-top:12px">
<div class="field"><label>จำนวนสุนัข</label><input id="dogCount" class="input" type="number" min="1"></div>
<div class="field"><label>สถานะ</label><select id="dogChain" class="select"><option>ไม่ได้ล่าม</option><option>ล่ามอยู่</option><option>ไม่แน่ใจ</option></select></div>
<div class="field"><label>จุดที่พบ</label><input id="dogLocation" class="input" placeholder="เช่น หน้าประตูรั้ว"></div>
<div class="field"><label>ช่วงเวลาที่พบ</label><input id="dogTime" class="input" type="time"></div>
</div>
</div>
<div class="step-actions"><button class="btn" onclick="prevStep()">← ย้อนกลับ</button><button class="btn btn-primary" onclick="nextStep()">ตรวจข้อมูลซ้ำ →</button></div>
</div>
<div class="step-panel" data-step-panel="6">
<div class="card-title"><div><h3>ตรวจสอบข้อมูลที่อาจซ้ำ</h3><div class="muted small">ตรวจจาก GPS เบอร์โทร โฉนด ที่อยู่ โครงการ และข้อมูลใกล้เคียง</div></div><button class="btn btn-primary" onclick="runDuplicateDetection()">ตรวจสอบอีกครั้ง</button></div>
<div id="duplicateDetectionSummary" class="alert info">กด “ตรวจสอบอีกครั้ง” เพื่อค้นหารายการที่อาจเป็นรายการเดียวกัน</div>
<div id="duplicateMatches" class="list"></div>
<div class="step-actions"><button class="btn" onclick="prevStep()">← ย้อนกลับ</button><button class="btn btn-primary" onclick="nextStep()">ยืนยันและตรวจทาน →</button></div>
</div>
<div class="step-panel" data-step-panel="7">
<div class="card-title"><div><h3>ตรวจสอบและบันทึก</h3><div class="muted small">ข้อมูลจะเข้าสู่คลังกลางเมื่อคุณกดยืนยัน</div></div></div>
<div id="reviewSummary"></div>
<div class="row" style="justify-content:flex-end;margin-top:20px">
<button class="btn" onclick="saveDraft()">บันทึกเป็นฉบับร่าง</button>
<button class="btn btn-danger" onclick="resetForm()">ยกเลิก</button>
<button class="btn btn-primary" onclick="saveRecord()">บันทึกข้อมูล</button>
</div>
</div>
</div>
</section>
<!-- STORAGE -->
<section class="page" id="page-storage">
<div class="grid grid-2">
<div class="storage-visual">
<div class="chip yellow">Central Repository</div>
<h2 style="margin:14px 0 6px">คลังข้อมูลกลางของทีม</h2>
<p style="color:#D4CCCA;margin:0">Single Source of Truth สำหรับข้อมูลทรัพย์ รูปภาพ เอกสาร GPS ประวัติสำรวจ งาน และจุดเตือน</p>
<div class="storage-meter"><span></span></div>
<div class="spread"><span>ใช้แล้ว 63 GB จาก 100 GB</span><strong>63%</strong></div>
</div>
<div class="card">
<div class="card-title"><h3>สถานะข้อมูล</h3><span class="chip green" id="storageSyncChip">Sync แล้ว</span></div>
<div class="grid grid-3">
<div class="stat-card card"><div class="label">รายการข้อมูล</div><div class="value" id="storageRecordCount">248</div></div>
<div class="stat-card card"><div class="label">รูปภาพ</div><div class="value">2,184</div></div>
<div class="stat-card card"><div class="label">เอกสาร</div><div class="value">164</div></div>
</div>
<div class="alert info" style="margin-top:14px">Prototype นี้จัดเก็บ Draft และรายการที่รอ Sync ใน Local Storage ของ Browser</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title">
<div><h3>ถังเก็บข้อมูล</h3><div class="muted small">จัดหมวดข้อมูลเพื่อค้นหาและนำกลับมาใช้ได้ง่าย</div></div>
<button class="btn btn-primary" onclick="createFolder()">+ สร้างหมวด</button>
</div>
<div class="folder-grid" id="folderGrid"></div>
</div>
<div class="grid grid-2" style="margin-top:16px">
<div class="card">
<div class="card-title"><h3>กิจกรรมล่าสุด</h3><button class="btn btn-sm" onclick="toast('เปิด Activity Timeline แล้ว')">ดูทั้งหมด</button></div>
<div id="storageActivity" class="list"></div>
</div>
<div class="card">
<div class="card-title"><h3>รายการรอ Sync</h3><button class="btn btn-sm btn-primary" onclick="syncPendingData()">Sync ตอนนี้</button></div>
<div id="pendingSyncList" class="list"></div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title">
<div><h3>รายการล่าสุดในคลัง</h3><div class="muted small">รองรับ Version History, Favorite และ Restore ใน Backend จริง</div></div>
<div class="row">
<button class="btn btn-sm" onclick="toast('แสดงรายการโปรดแล้ว')">★ รายการโปรด</button>
<button class="btn btn-sm" onclick="toast('แสดงรายการที่เปิดล่าสุดแล้ว')">ดูล่าสุด</button>
</div>
</div>
<div id="storageTableWrap"></div>
</div>
</section>
<!-- TEAM -->
<section class="page" id="page-team">
<div class="grid grid-4">
<div class="card stat-card yellow"><div class="label">งานของฉัน</div><div class="value">8</div></div>
<div class="card stat-card info"><div class="label">ลงพื้นที่วันนี้</div><div class="value">5</div></div>
<div class="card stat-card warning"><div class="label">รอตรวจสอบ</div><div class="value">7</div></div>
<div class="card stat-card success"><div class="label">เสร็จสิ้นสัปดาห์นี้</div><div class="value">19</div></div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title">
<div><h3>งานของทีม</h3><div class="muted small">เปลี่ยนสถานะ มอบหมายงาน และเปิดตำแหน่งได้</div></div>
<button class="btn btn-primary" onclick="openAssignTaskModal()">+ มอบหมายงาน</button>
</div>
<div id="taskTable"></div>
</div>
</section>
<!-- SAFETY -->
<section class="page" id="page-safety">
<div id="proximityAlert" class="alert danger hidden"></div>
<div class="card">
<div class="toolbar">
<select id="safetySeverityFilter" class="select" style="max-width:210px" onchange="renderSafetyList()">
<option value="">ทุกระดับ</option><option>ข้อมูลทั่วไป</option><option>ควรระวัง</option><option>เสี่ยงสูง</option>
</select>
<select id="safetyTypeFilter" class="select" style="max-width:210px" onchange="renderSafetyList()">
<option value="">ทุกประเภท</option><option>สุนัขดุ</option><option>ทางเข้าแคบ</option><option>น้ำท่วม</option><option>ถนนชำรุด</option><option>พื้นที่เปลี่ยว</option>
</select>
<select id="safetyActiveFilter" class="select" style="max-width:210px" onchange="renderSafetyList()">
<option value="">ทั้งหมด</option><option value="active">ยังมีอยู่</option><option value="resolved">แก้ไขแล้ว</option>
</select>
<button class="btn btn-primary" onclick="openSafetyModal()">+ เพิ่มจุดเตือน</button>
</div>
<div class="grid dashboard-grid">
<div id="safetyMap" class="map"></div>
<div>
<div class="card-title"><div><h3>จุดเตือนใกล้คุณ</h3><div class="muted small">เรียงตามระยะจากตำแหน่งปัจจุบัน</div></div></div>
<div id="safetyList" class="list" style="max-height:520px;overflow:auto"></div>
</div>
</div>
</div>
</section>
<!-- NEARBY -->
<section class="page" id="page-nearby">
<div class="hero">
<h2>พักไหนดีใกล้ฉัน ☕</h2>
<p>ตัวช่วยหาร้านอาหาร คาเฟ่ ห้องน้ำ ปั๊มน้ำมัน ATM และจุดจอดรถสำหรับเจ้าหน้าที่ภาคสนาม</p>
<button class="btn btn-primary" onclick="requestLocationWithExplanation()">ใช้ตำแหน่งปัจจุบัน</button>
</div>
<div class="card" style="margin-top:16px">
<div class="toolbar" id="nearbyCategoryButtons"></div>
<div class="alert info">ข้อมูลสถานที่ใน Prototype เป็นข้อมูลตัวอย่าง สามารถเปลี่ยนเป็น Places API หรือ OpenStreetMap Overpass API ได้ในระบบจริง</div>
<div class="grid content-sidebar">
<div id="nearbyMap" class="map"></div>
<div id="nearbyList" class="list" style="max-height:540px;overflow:auto"></div>
</div>
</div>
</section>
<!-- REPORTS -->
<section class="page" id="page-reports">
<div class="grid grid-4">
<div class="card stat-card success"><div class="label">ลดเวลาค้นหาข้อมูล</div><div class="value">80%</div></div>
<div class="card stat-card success"><div class="label">ลดเวลาจัดทำรายงาน</div><div class="value">50%</div></div>
<div class="card stat-card yellow"><div class="label">ข้อมูลถูกนำกลับมาใช้</div><div class="value">126</div></div>
<div class="card stat-card info"><div class="label">ป้องกันลงพื้นที่ซ้ำ</div><div class="value">24</div></div>
</div>
<div class="grid grid-2" style="margin-top:16px">
<div class="card">
<div class="card-title"><h3>ตัวชี้วัดการปฏิบัติงาน</h3></div>
<table>
<tbody>
<tr><td>จำนวนข้อมูลภาคสนาม</td><td><strong>248 รายการ</strong></td></tr>
<tr><td>ผู้ใช้งานที่ Active</td><td><strong>38 คน</strong></td></tr>
<tr><td>ข้อมูลที่นำกลับมาใช้</td><td><strong>126 ครั้ง</strong></td></tr>
<tr><td>รายการซ้ำที่ตรวจพบ</td><td><strong>31 รายการ</strong></td></tr>
<tr><td>GPS Capture Success Rate</td><td><strong>94%</strong></td></tr>
<tr><td>Camera Capture Success Rate</td><td><strong>96%</strong></td></tr>
</tbody>
</table>
</div>
<div class="card">
<div class="card-title"><h3>จุดเตือนตามประเภท</h3></div>
<div class="stack">
<div><div class="spread"><span>สุนัขดุ</span><strong>12</strong></div><div class="progress"><span style="width:75%"></span></div></div>
<div><div class="spread"><span>ทางเข้าแคบ</span><strong>9</strong></div><div class="progress"><span style="width:58%"></span></div></div>
<div><div class="spread"><span>ถนนชำรุด</span><strong>7</strong></div><div class="progress"><span style="width:44%"></span></div></div>
<div><div class="spread"><span>น้ำท่วม</span><strong>5</strong></div><div class="progress"><span style="width:31%"></span></div></div>
<div><div class="spread"><span>ไม่มีที่จอดรถ</span><strong>3</strong></div><div class="progress"><span style="width:19%"></span></div></div>
</div>
</div>
</div>
<div class="card" style="margin-top:16px">
<div class="card-title"><h3>ผลงานของทีม</h3><button class="btn btn-sm" onclick="toast('ดาวน์โหลดรายงานแล้ว')">ดาวน์โหลดรายงาน</button></div>
<div class="grid grid-4">
<div class="folder"><strong>คุณปอ</strong><small>46 รายการ · ยืนยัน 38</small></div>
<div class="folder"><strong>คุณเมย์</strong><small>41 รายการ · ยืนยัน 35</small></div>
<div class="folder"><strong>คุณต้น</strong><small>37 รายการ · ยืนยัน 29</small></div>
<div class="folder"><strong>คุณฝน</strong><small>34 รายการ · ยืนยัน 27</small></div>
</div>
</div>
</section>
</main>
</div>
<nav class="bottom-nav" id="mobileNav">
<button class="active" data-page="dashboard"><span>⌂</span>ภาพรวม</button>
<button data-page="map"><span>⌖</span>แผนที่</button>
<button class="add-mobile" data-page="add">+</button>
<button data-page="storage"><span>▣</span>คลังกลาง</button>
<button data-page="team"><span>♙</span>งาน</button>
</nav>
<!-- CAMERA PERMISSION MODAL -->
<div class="modal" id="cameraPermissionModal">
<div class="modal-card">
<div class="modal-header"><h3>อนุญาตให้ใช้กล้อง</h3><button class="icon-btn" onclick="closeModal('cameraPermissionModal')">✕</button></div>
<p>Fieldmate ใช้กล้องเพื่อบันทึกป้ายประกาศ สภาพทรัพย์ ทางเข้า และจุดที่ต้องระวัง</p>
<div class="alert info">คุณสามารถหยุดกล้องได้ทุกเมื่อ และภาพจะไม่ถูกบันทึกจนกว่าจะกดยืนยัน</div>
<div class="row" style="justify-content:flex-end">
<button class="btn" onclick="closeModal('cameraPermissionModal')">ยังไม่อนุญาต</button>
<button class="btn btn-primary" onclick="startCamera()">อนุญาตและดำเนินการต่อ</button>
</div>
</div>
</div>
<!-- CAMERA MODAL -->
<div class="modal" id="cameraModal">
<div class="modal-card wide">
<div class="modal-header"><h3>กล้องภาคสนาม</h3><button class="icon-btn" onclick="closeCamera()">✕</button></div>
<div class="camera-box">
<div class="camera-active">● กล้องกำลังทำงาน</div>
<video id="cameraVideo" autoplay playsinline></video>
<canvas id="cameraCanvas" class="hidden"></canvas>
<div id="cameraError" class="hidden" style="padding:30px;text-align:center"></div>
</div>
<div class="toolbar" style="margin-top:14px;justify-content:center">
<button class="btn" onclick="switchCamera()">↻ สลับกล้อง</button>
<button class="btn btn-primary" onclick="capturePhoto()">● ถ่ายภาพ</button>
<label class="btn">อัปโหลดแทน<input type="file" accept="image/*" capture="environment" hidden onchange="handleCameraFallback(event)"></label>
</div>
</div>
</div>
<!-- LOCATION PERMISSION MODAL -->
<div class="modal" id="locationPermissionModal">
<div class="modal-card">
<div class="modal-header"><h3>อนุญาตให้ใช้ตำแหน่ง</h3><button class="icon-btn" onclick="closeModal('locationPermissionModal')">✕</button></div>
<p>Fieldmate ใช้ตำแหน่งของคุณเพื่อค้นหาข้อมูลเดิม ปักหมุดพื้นที่สำรวจ และแจ้งจุดที่ต้องระวังใกล้เคียง</p>
<div class="alert info">คุณสามารถหยุดการติดตามตำแหน่งได้ทุกเมื่อ และระบบจะไม่ติดตามต่อหลังจากกดหยุด</div>
<div class="row" style="justify-content:flex-end">
<button class="btn" onclick="closeModal('locationPermissionModal')">ยังไม่อนุญาต</button>
<button class="btn btn-primary" onclick="getCurrentLocation()">อนุญาตและดำเนินการต่อ</button>
</div>
</div>
</div>
<!-- RECORD DETAIL MODAL -->
<div class="modal" id="recordModal">
<div class="modal-card wide">
<div class="modal-header"><h3>รายละเอียดข้อมูลภาคสนาม</h3><button class="icon-btn" onclick="closeModal('recordModal')">✕</button></div>
<div id="recordDetailContent"></div>
</div>
</div>
<!-- ASSIGN TASK MODAL -->
<div class="modal" id="assignTaskModal">
<div class="modal-card">
<div class="modal-header"><h3>มอบหมายงานสำรวจ</h3><button class="icon-btn" onclick="closeModal('assignTaskModal')">✕</button></div>
<div class="stack">
<div class="field"><label>รายการหรือพื้นที่</label><input id="taskRecordInput" class="input" placeholder="เช่น FM-2026-001"></div>
<div class="field"><label>ผู้รับผิดชอบ</label><select id="taskAssigneeInput" class="select"><option>คุณปอ</option><option>คุณเมย์</option><option>คุณต้น</option><option>คุณฝน</option></select></div>
<div class="field"><label>วันครบกำหนด</label><input id="taskDueInput" class="input" type="date"></div>
<div class="field"><label>รายละเอียด</label><textarea id="taskNoteInput" class="textarea"></textarea></div>
<button class="btn btn-primary" onclick="createTask()">มอบหมายงาน</button>
</div>
</div>
</div>
<!-- SAFETY MODAL -->
<div class="modal" id="safetyModal">
<div class="modal-card">
<div class="modal-header"><h3>เพิ่มจุดเตือน</h3><button class="icon-btn" onclick="closeModal('safetyModal')">✕</button></div>
<div class="field-grid">
<div class="field"><label>ประเภท</label><select id="modalWarningType" class="select"><option>สุนัขดุ</option><option>ทางเข้าแคบ</option><option>น้ำท่วม</option><option>ถนนชำรุด</option><option>พื้นที่เปลี่ยว</option></select></div>
<div class="field"><label>ระดับ</label><select id="modalWarningSeverity" class="select"><option>ข้อมูลทั่วไป</option><option>ควรระวัง</option><option>เสี่ยงสูง</option></select></div>
<div class="field"><label>Latitude</label><input id="modalWarningLat" class="input" type="number" step="any"></div>
<div class="field"><label>Longitude</label><input id="modalWarningLng" class="input" type="number" step="any"></div>
</div>
<div class="field" style="margin-top:14px"><label>รายละเอียด</label><textarea id="modalWarningDescription" class="textarea"></textarea></div>
<button class="btn btn-primary full" style="margin-top:14px" onclick="saveSafetyWarning()">บันทึกจุดเตือน</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
/* =========================================================
FIELD MATE PROTOTYPE
- Mock data อยู่ใน JavaScript
- Draft / Pending Sync ใช้ localStorage
- จุดเชื่อม Backend จริงควรแทนที่ records, warnings, places, tasks
========================================================= */
// ---------- MOCK DATA ----------
const placeholderImages = [
"https://images.unsplash.com/photo-1564013799919-ab600027ffc6?auto=format&fit=crop&w=600&q=70",
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=600&q=70",
"https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=600&q=70",
"https://images.unsplash.com/photo-1600047509807-ba8f99d2cdde?auto=format&fit=crop&w=600&q=70"
];
let records = [
{id:"FM-2026-001",type:"บ้านเดี่ยว",area:"บางนา",address:"ซอยบางนา-ตราด 23 เขตบางนา กรุงเทพมหานคร",lat:13.6678,lng:100.6351,price:"7,900,000 บาท",land:"58 ตร.ว.",usable:"210 ตร.ม.",phone:"0892345678",contact:"คุณเอ",surveyDate:"2026-07-12",updated:"2026-07-16",createdBy:"คุณปอ",editedBy:"คุณเมย์",status:"ยืนยันแล้ว",notes:"เข้าทางซอยหลักได้ สภาพทรัพย์ใช้งานได้",access:"รถยนต์เข้าได้",parking:"จอดในบ้าน 2 คัน",signal:"ดี",warnings:["สุนัขดุ"],project:"บ้านสวนบางนา",titleDeed:"12345",images:[placeholderImages[0]],history:["สำรวจครั้งแรก 12 ก.ค. 2569","ยืนยันตำแหน่ง 16 ก.ค. 2569"],comments:["ควรโทรแจ้งก่อนเข้าพื้นที่"]},
{id:"FM-2026-002",type:"ทาวน์โฮม",area:"ศรีนครินทร์",address:"ถนนศรีนครินทร์ ซอย 42 เขตประเวศ กรุงเทพมหานคร",lat:13.6920,lng:100.6462,price:"",land:"22 ตร.ว.",usable:"120 ตร.ม.",phone:"0817788990",contact:"คุณบี",surveyDate:"2026-07-08",updated:"2026-07-15",createdBy:"คุณเมย์",editedBy:"คุณเมย์",status:"ข้อมูลใหม่",notes:"ทางเข้าโครงการชัดเจน",access:"รถยนต์เข้าได้",parking:"หน้าบ้าน 1 คัน",signal:"ดี",warnings:[],project:"เดอะคอนเนค ศรีนครินทร์",titleDeed:"22348",images:[placeholderImages[1]],history:["สำรวจครั้งแรก 8 ก.ค. 2569"],comments:[]},
{id:"FM-2026-003",type:"ที่ดินเปล่า",area:"ลาดกระบัง",address:"ถนนหลวงแพ่ง เขตลาดกระบัง กรุงเทพมหานคร",lat:13.7180,lng:100.7802,price:"12,000,000 บาท",land:"2 ไร่ 1 งาน",usable:"-",phone:"0861112223",contact:"คุณซี",surveyDate:"2026-06-22",updated:"2026-06-22",createdBy:"คุณต้น",editedBy:"คุณต้น",status:"ควรสำรวจซ้ำ",notes:"หน้าฝนทางเข้ามีน้ำขัง",access:"ทางเข้าแคบ",parking:"จอดริมถนน",signal:"ปานกลาง",warnings:["น้ำท่วม","ถนนลูกรัง"],project:"",titleDeed:"33490",images:[placeholderImages[2]],history:["สำรวจ 22 มิ.ย. 2569"],comments:["ตรวจซ้ำหลังฤดูฝน"]},
{id:"FM-2026-004",type:"คอนโดมิเนียม",area:"พระโขนง",address:"ถนนสุขุมวิท 71 เขตวัฒนา กรุงเทพมหานคร",lat:13.7134,lng:100.5958,price:"3,250,000 บาท",land:"-",usable:"31 ตร.ม.",phone:"0924455667",contact:"คุณดี",surveyDate:"2026-07-01",updated:"2026-07-14",createdBy:"คุณฝน",editedBy:"คุณปอ",status:"ยืนยันแล้ว",notes:"ติดต่อสำนักงานนิติบุคคลก่อนเข้า",access:"รถยนต์เข้าได้",parking:"ต้องแลกบัตร",signal:"ดี",warnings:[],project:"The Line Sukhumvit 71",titleDeed:"CN-4490",images:[placeholderImages[3]],history:["สำรวจ 1 ก.ค. 2569","Reviewer ยืนยัน 14 ก.ค. 2569"],comments:["ข้อมูลส่วนกลางครบ"]},
{id:"FM-2026-005",type:"โกดัง",area:"บางพลี",address:"ถนนเทพารักษ์ อำเภอบางพลี สมุทรปราการ",lat:13.6052,lng:100.7015,price:"",land:"1 ไร่",usable:"1,200 ตร.ม.",phone:"0909876543",contact:"คุณอี",surveyDate:"2026-06-28",updated:"2026-07-10",createdBy:"คุณปอ",editedBy:"คุณต้น",status:"อาจซ้ำ",notes:"พบข้อมูลใกล้เคียงอีก 1 รายการ",access:"รถบรรทุกเข้าได้",parking:"พื้นที่กว้าง",signal:"ดี",warnings:["พื้นที่ก่อสร้าง"],project:"BP Warehouse",titleDeed:"55901",images:[placeholderImages[0]],history:["สำรวจ 28 มิ.ย. 2569"],comments:["ตรวจเลขที่โฉนดอีกครั้ง"]},
{id:"FM-2026-006",type:"อาคารพาณิชย์",area:"สมุทรปราการ",address:"ถนนศรีสมุทร อำเภอเมือง สมุทรปราการ",lat:13.5996,lng:100.5968,price:"6,500,000 บาท",land:"18 ตร.ว.",usable:"160 ตร.ม.",phone:"0951119090",contact:"คุณเอฟ",surveyDate:"2026-06-30",updated:"2026-07-09",createdBy:"คุณเมย์",editedBy:"คุณฝน",status:"ยืนยันแล้ว",notes:"จอดรถยากช่วงกลางวัน",access:"รถยนต์เข้าได้",parking:"จำกัด",signal:"ดี",warnings:["ที่จอดรถจำกัด"],project:"",titleDeed:"66123",images:[placeholderImages[1]],history:["สำรวจ 30 มิ.ย. 2569"],comments:[]},
{id:"FM-2026-007",type:"บ้านเดี่ยว",area:"บางพลี",address:"กิ่งแก้ว 19 อำเภอบางพลี สมุทรปราการ",lat:13.6464,lng:100.7284,price:"",land:"65 ตร.ว.",usable:"230 ตร.ม.",phone:"0892345678",contact:"คุณจี",surveyDate:"2026-07-02",updated:"2026-07-02",createdBy:"คุณต้น",editedBy:"คุณต้น",status:"อาจซ้ำ",notes:"เบอร์โทรซ้ำกับ FM-2026-001",access:"รถยนต์เข้าได้",parking:"ในบ้าน",signal:"ปานกลาง",warnings:["สุนัขดุ"],project:"บ้านกิ่งแก้ว",titleDeed:"77100",images:[placeholderImages[2]],history:["สำรวจ 2 ก.ค. 2569"],comments:["ตรวจสอบผู้ติดต่อ"]},
{id:"FM-2026-008",type:"โรงงาน",area:"บางนา",address:"ถนนบางนา-ตราด กม. 18 สมุทรปราการ",lat:13.6252,lng:100.7821,price:"",land:"3 ไร่",usable:"2,800 ตร.ม.",phone:"0825556777",contact:"คุณเอช",surveyDate:"2026-05-16",updated:"2026-06-20",createdBy:"คุณฝน",editedBy:"คุณเมย์",status:"ควรสำรวจซ้ำ",notes:"ต้องนัดหมายล่วงหน้า 2 วัน",access:"รถบรรทุกเข้าได้",parking:"มีลานจอด",signal:"ดี",warnings:["พื้นที่ห้ามเข้า"],project:"Bangna Industrial",titleDeed:"88991",images:[placeholderImages[3]],history:["สำรวจ 16 พ.ค. 2569"],comments:["ใบอนุญาตเข้าพื้นที่หมดอายุ"]},
{id:"FM-2026-009",type:"ทาวน์โฮม",area:"พระโขนง",address:"ซอยปุณณวิถี 20 เขตพระโขนง กรุงเทพมหานคร",lat:13.6861,lng:100.6119,price:"4,200,000 บาท",land:"19 ตร.ว.",usable:"135 ตร.ม.",phone:"0801122334",contact:"คุณไอ",surveyDate:"2026-07-11",updated:"2026-07-11",createdBy:"คุณปอ",editedBy:"คุณปอ",status:"ข้อมูลใหม่",notes:"ถนนในซอยค่อนข้างแคบ",access:"ทางเข้าแคบ",parking:"หน้าบ้าน",signal:"ดี",warnings:["ทางเข้าแคบ"],project:"Punnawithi Home",titleDeed:"99012",images:[placeholderImages[0]],history:["สำรวจ 11 ก.ค. 2569"],comments:[]},
{id:"FM-2026-010",type:"คอนโดมิเนียม",area:"ศรีนครินทร์",address:"ถนนศรีนครินทร์ ใกล้ MRT สวนหลวง ร.9",lat:13.6987,lng:100.6428,price:"2,850,000 บาท",land:"-",usable:"28 ตร.ม.",phone:"0849988776",contact:"คุณเจ",surveyDate:"2026-07-13",updated:"2026-07-16",createdBy:"คุณเมย์",editedBy:"คุณปอ",status:"ยืนยันแล้ว",notes:"เข้าอาคารผ่านนิติบุคคล",access:"รถยนต์เข้าได้",parking:"Visitor 2 ชั่วโมง",signal:"ดี",warnings:[],project:"Rich Park Triple Station",titleDeed:"CN-10012",images:[placeholderImages[1]],history:["สำรวจ 13 ก.ค. 2569","อัปเดตภาพ 16 ก.ค. 2569"],comments:["ข้อมูลครบ"]},
{id:"FM-2026-011",type:"ที่ดินเปล่า",area:"ลาดกระบัง",address:"ซอยฉลองกรุง 31 เขตลาดกระบัง กรุงเทพมหานคร",lat:13.7298,lng:100.7902,price:"",land:"3 งาน 40 ตร.ว.",usable:"-",phone:"0886677889",contact:"คุณเค",surveyDate:"2026-06-15",updated:"2026-06-15",createdBy:"คุณฝน",editedBy:"คุณฝน",status:"ฉบับร่าง",notes:"สัญญาณโทรศัพท์ไม่เสถียร",access:"รถยนต์เข้าได้",parking:"จอดข้างทาง",signal:"อ่อน",warnings:["ไม่มีสัญญาณโทรศัพท์"],project:"",titleDeed:"11145",images:[placeholderImages[2]],history:["บันทึกร่าง 15 มิ.ย. 2569"],comments:[]},
{id:"FM-2026-012",type:"อาคารสำนักงาน",area:"บางนา",address:"ถนนเทพรัตน เขตบางนา กรุงเทพมหานคร",lat:13.6689,lng:100.6359,price:"",land:"45 ตร.ว.",usable:"520 ตร.ม.",phone:"0834455990",contact:"คุณแอล",surveyDate:"2026-07-16",updated:"2026-07-16",createdBy:"คุณต้น",editedBy:"คุณต้น",status:"ข้อมูลใหม่",notes:"อยู่ใกล้ FM-2026-001 ประมาณ 130 เมตร",access:"รถยนต์เข้าได้",parking:"4 คัน",signal:"ดี",warnings:[],project:"Bangna Office Place",titleDeed:"12190",images:[placeholderImages[3]],history:["สำรวจ 16 ก.ค. 2569"],comments:[]}
];
let warnings = [
{id:"W-001",type:"สุนัขดุ",severity:"เสี่ยงสูง",lat:13.6684,lng:100.6356,distance:120,date:"2026-07-16",reporter:"คุณปอ",description:"สุนัข 2 ตัว ไม่ได้ล่าม อยู่หน้าประตูรั้ว",active:true,confirm:6,lastConfirm:"วันนี้ 09:18",image:placeholderImages[0]},
{id:"W-002",type:"ทางเข้าแคบ",severity:"ควรระวัง",lat:13.6865,lng:100.6124,distance:450,date:"2026-07-15",reporter:"คุณเมย์",description:"รถใหญ่กลับรถยาก แนะนำจอดปากซอย",active:true,confirm:3,lastConfirm:"เมื่อวาน 16:30",image:placeholderImages[1]},
{id:"W-003",type:"น้ำท่วม",severity:"เสี่ยงสูง",lat:13.7186,lng:100.7805,distance:820,date:"2026-07-12",reporter:"คุณต้น",description:"น้ำขังช่วงฝนตกหนัก รถเล็กไม่ควรเข้า",active:true,confirm:8,lastConfirm:"12 ก.ค. 14:10",image:placeholderImages[2]},
{id:"W-004",type:"ถนนชำรุด",severity:"ควรระวัง",lat:13.6460,lng:100.7290,distance:1100,date:"2026-07-10",reporter:"คุณฝน",description:"มีหลุมลึกช่วงก่อนถึงโครงการ",active:true,confirm:4,lastConfirm:"10 ก.ค. 11:05",image:placeholderImages[3]},
{id:"W-005",type:"พื้นที่เปลี่ยว",severity:"ควรระวัง",lat:13.7301,lng:100.7907,distance:2100,date:"2026-07-09",reporter:"คุณปอ",description:"แนะนำลงพื้นที่ก่อน 17:00",active:true,confirm:2,lastConfirm:"9 ก.ค. 15:40",image:placeholderImages[0]},
{id:"W-006",type:"ที่จอดรถจำกัด",severity:"ข้อมูลทั่วไป",lat:13.5998,lng:100.5964,distance:3200,date:"2026-07-05",reporter:"คุณเมย์",description:"จอดได้เฉพาะช่วง 10:00–14:00",active:true,confirm:5,lastConfirm:"8 ก.ค. 10:00",image:placeholderImages[1]},
{id:"W-007",type:"พื้นที่ก่อสร้าง",severity:"ควรระวัง",lat:13.6055,lng:100.7010,distance:4600,date:"2026-07-03",reporter:"คุณต้น",description:"มีรถบรรทุกเข้าออกตลอดวัน",active:true,confirm:3,lastConfirm:"7 ก.ค. 13:20",image:placeholderImages[2]},
{id:"W-008",type:"ไม่มีสัญญาณโทรศัพท์",severity:"ข้อมูลทั่วไป",lat:13.7294,lng:100.7898,distance:5100,date:"2026-06-30",reporter:"คุณฝน",description:"บางเครือข่ายไม่มีสัญญาณ",active:true,confirm:2,lastConfirm:"2 ก.ค. 09:45",image:placeholderImages[3]},
{id:"W-009",type:"รถยนต์เข้าไม่ได้",severity:"เสี่ยงสูง",lat:13.6248,lng:100.7815,distance:6500,date:"2026-06-28",reporter:"คุณปอ",description:"ถนนปิดชั่วคราว ต้องเดินประมาณ 300 เมตร",active:false,confirm:7,lastConfirm:"แก้ไขแล้ว 5 ก.ค.",image:placeholderImages[0]},
{id:"W-010",type:"เสี่ยงอุบัติเหตุ",severity:"ควรระวัง",lat:13.6925,lng:100.6458,distance:900,date:"2026-07-14",reporter:"คุณเมย์",description:"ทางโค้งไม่มีไฟส่องสว่าง",active:true,confirm:4,lastConfirm:"14 ก.ค. 18:20",image:placeholderImages[1]}
];
let places = [
{id:"P01",name:"ครัวบางนา",category:"ร้านอาหาร",lat:13.6687,lng:100.6346,distance:180,time:"3 นาที",open:true,rating:4.3,description:"อาหารตามสั่ง มีที่จอดรถ"},
{id:"P02",name:"บ้านกาแฟสนาม",category:"ร้านกาแฟ",lat:13.6670,lng:100.6338,distance:240,time:"4 นาที",open:true,rating:4.5,description:"มีปลั๊กและห้องน้ำ"},
{id:"P03",name:"7-Eleven บางนา 23",category:"ร้านสะดวกซื้อ",lat:13.6691,lng:100.6362,distance:260,time:"4 นาที",open:true,rating:4.0,description:"เปิด 24 ชั่วโมง"},
{id:"P04",name:"ห้องน้ำสาธารณะ ศูนย์บริการ",category:"ห้องน้ำ",lat:13.6665,lng:100.6368,distance:340,time:"5 นาที",open:true,rating:null,description:"ข้อมูลตัวอย่าง"},
{id:"P05",name:"PTT Station บางนา",category:"ปั๊มน้ำมัน",lat:13.6710,lng:100.6329,distance:520,time:"6 นาที",open:true,rating:4.2,description:"มีร้านอาหารและห้องน้ำ"},
{id:"P06",name:"ATM Krungsri",category:"ATM",lat:13.6682,lng:100.6373,distance:300,time:"4 นาที",open:true,rating:null,description:"หน้าร้านสะดวกซื้อ"},
{id:"P07",name:"โรงพยาบาลไทยนครินทร์",category:"โรงพยาบาล",lat:13.6698,lng:100.6340,distance:680,time:"8 นาที",open:true,rating:4.1,description:"เปิด 24 ชั่วโมง"},
{id:"P08",name:"สน.บางนา",category:"สถานีตำรวจ",lat:13.6648,lng:100.6410,distance:1100,time:"12 นาที",open:true,rating:null,description:"สถานีตำรวจใกล้พื้นที่"},
{id:"P09",name:"ลานจอดรถชุมชน",category:"จุดจอดรถ",lat:13.6674,lng:100.6328,distance:400,time:"5 นาที",open:true,rating:3.9,description:"คิดค่าบริการรายชั่วโมง"},
{id:"P10",name:"ก๋วยเตี๋ยวเรือคุณนิด",category:"ร้านอาหาร",lat:13.6669,lng:100.6370,distance:360,time:"5 นาที",open:false,rating:4.4,description:"ปิดทุกวันจันทร์"},
{id:"P11",name:"Yellow Cup Café",category:"ร้านกาแฟ",lat:13.6694,lng:100.6334,distance:410,time:"5 นาที",open:true,rating:4.6,description:"นั่งทำงานได้"},
{id:"P12",name:"Mini Big C",category:"ร้านสะดวกซื้อ",lat:13.6658,lng:100.6345,distance:590,time:"7 นาที",open:true,rating:4.1,description:"มี ATM"},
{id:"P13",name:"Shell ศรีนครินทร์",category:"ปั๊มน้ำมัน",lat:13.6915,lng:100.6480,distance:2100,time:"9 นาที",open:true,rating:4.0,description:"ข้อมูลตัวอย่าง"},
{id:"P14",name:"ตลาดนัดบางนา",category:"ร้านอาหาร",lat:13.6702,lng:100.6385,distance:780,time:"10 นาที",open:true,rating:4.2,description:"ร้านอาหารหลายร้าน"},
{id:"P15",name:"อาคารจอดรถ B1",category:"จุดจอดรถ",lat:13.6708,lng:100.6350,distance:620,time:"7 นาที",open:true,rating:null,description:"มี รปภ. ดูแล"}
];
let tasks = [
{id:"T-101",record:"FM-2026-003",title:"สำรวจซ้ำที่ดินถนนหลวงแพ่ง",assignee:"คุณปอ",due:"วันนี้ 14:00",status:"รอดำเนินการ",area:"ลาดกระบัง"},
{id:"T-102",record:"FM-2026-008",title:"ตรวจสอบใบอนุญาตเข้าพื้นที่",assignee:"คุณเมย์",due:"วันนี้ 15:30",status:"กำลังเดินทาง",area:"บางนา"},
{id:"T-103",record:"FM-2026-005",title:"ตรวจเลขที่โฉนดรายการอาจซ้ำ",assignee:"คุณต้น",due:"วันนี้ 17:00",status:"รอตรวจสอบ",area:"บางพลี"},
{id:"T-104",record:"FM-2026-011",title:"เก็บภาพทางเข้าเพิ่มเติม",assignee:"คุณฝน",due:"18 ก.ค. 2569",status:"กำลังลงพื้นที่",area:"ลาดกระบัง"},
{id:"T-105",record:"FM-2026-006",title:"ยืนยันข้อจำกัดที่จอดรถ",assignee:"คุณปอ",due:"19 ก.ค. 2569",status:"รอดำเนินการ",area:"สมุทรปราการ"}
];
const folders = [
{icon:"🏠",name:"Property Records",count:"248 รายการ"},
{icon:"🖼️",name:"Images",count:"2,184 ไฟล์"},
{icon:"📄",name:"Documents",count:"164 ไฟล์"},
{icon:"📍",name:"GPS & Map",count:"248 จุด"},
{icon:"🕘",name:"Survey History",count:"612 เหตุการณ์"},
{icon:"⚠️",name:"Safety Reports",count:"36 จุด"},
{icon:"💬",name:"Team Comments",count:"189 ความคิดเห็น"},
{icon:"✅",name:"Tasks",count:"52 งาน"},
{icon:"📎",name:"Attachments",count:"431 ไฟล์"},
{icon:"↩️",name:"Version History",count:"1,024 เวอร์ชัน"}
];
// ---------- STATE ----------
let currentPage = 1;
let currentStep = 1;
let currentPosition = null;
let locationWatchId = null;
let cameraStream = null;
let cameraFacingMode = "environment";
let capturedPhotos = [];
let selectedNearbyCategory = "";
let maps = {};
let layers = {};
let manualPinMode = false;
let formPin = null;
// ---------- UTILITIES ----------
const $ = id => document.getElementById(id);
const formatDateThai = d => new Intl.DateTimeFormat("th-TH",{dateStyle:"long"}).format(new Date(d));
const todayISO = () => new Date().toISOString().slice(0,10);
const maskPhone = phone => phone ? phone.slice(0,3)+"-xxx-"+phone.slice(-4) : "—";
const safe = value => value ?? "—";
function toast(message){
const t = $("toast");
t.textContent = message;
t.classList.add("show");
clearTimeout(t._timer);
t._timer = setTimeout(()=>t.classList.remove("show"),2800);
}
function openModal(id){$(id).classList.add("open")}
function closeModal(id){$(id).classList.remove("open")}
function distanceMeters(lat1,lng1,lat2,lng2){
const R=6371000, p1=lat1*Math.PI/180, p2=lat2*Math.PI/180;
const dp=(lat2-lat1)*Math.PI/180, dl=(lng2-lng1)*Math.PI/180;
const a=Math.sin(dp/2)**2+Math.cos(p1)*Math.cos(p2)*Math.sin(dl/2)**2;
return 2*R*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
}
function markerColor(status){
return {"ข้อมูลใหม่":"#2D6CDF","ยืนยันแล้ว":"#20865A","ควรสำรวจซ้ำ":"#E67E22","อาจซ้ำ":"#C73B32","ฉบับร่าง":"#746D6A"}[status] || "#241918";
}
function divIcon(color,label=""){
return L.divIcon({
className:"",
html:`<div style="width:28px;height:28px;border-radius:50% 50% 50% 0;background:${color};border:3px solid #fff;box-shadow:0 3px 10px rgba(0,0,0,.28);transform:rotate(-45deg);display:grid;place-items:center"><span style="transform:rotate(45deg);font-size:12px">${label}</span></div>`,
iconSize:[30,30],iconAnchor:[15,28]
});
}
// ---------- NAVIGATION ----------
const pageTitles = {
dashboard:["ภาพรวม","ข้อมูลภาคสนามของทีมในที่เดียว"],
map:["แผนที่ข้อมูล","Smart Map พร้อมตำแหน่งจริง ข้อมูลเดิม จุดเตือน และสถานที่ใกล้เคียง"],
search:["ค้นหาข้อมูลกลาง","ค้นหาข้อมูลเดิมก่อนเดินทางหรือสร้างรายการใหม่"],
add:["เพิ่มข้อมูลภาคสนาม","ขั้นตอนบันทึกข้อมูลที่ตรวจสอบรายการซ้ำก่อนบันทึก"],
storage:["คลังข้อมูลกลาง","ถังเก็บข้อมูลและเอกสารแบบ Single Source of Truth"],
team:["งานของทีม","มอบหมาย ติดตาม และตรวจสอบงานภาคสนาม"],
safety:["แจ้งเตือนพื้นที่","แชร์ข้อมูลความเสี่ยงและข้อจำกัดการเข้าถึงให้ทีม"],
nearby:["พักไหนดีใกล้ฉัน","สถานที่ที่เป็นประโยชน์ระหว่างลงพื้นที่"],
reports:["รายงาน","ตัวชี้วัดด้านการปฏิบัติงานและการนำข้อมูลกลับมาใช้"]
};
function openPage(page){
document.querySelectorAll(".page").forEach(p=>p.classList.remove("active"));
$("page-"+page).classList.add("active");
document.querySelectorAll("[data-page]").forEach(b=>b.classList.toggle("active",b.dataset.page===page));
$("topPageTitle").textContent=pageTitles[page][0];
$("topPageSubtitle").textContent=pageTitles[page][1];
window.scrollTo({top:0,behavior:"smooth"});
setTimeout(()=>{
if(page==="map"){initMainMap();maps.main?.invalidateSize();renderMapMarkers()}
if(page==="add"){initFormMap();maps.form?.invalidateSize()}
if(page==="safety"){initSafetyMap();maps.safety?.invalidateSize();renderSafetyList()}
if(page==="nearby"){initNearbyMap();maps.nearby?.invalidateSize();renderNearbyPlaces()}
if(page==="search") renderSearchResults();
if(page==="storage") renderStorage();
if(page==="team") renderTasks();
},100);
}
document.querySelectorAll("[data-page]").forEach(btn=>btn.addEventListener("click",()=>openPage(btn.dataset.page)));
// ---------- MAP INITIALIZATION & FALLBACK ----------
function createMap(id,center=[13.6678,100.6351],zoom=13){
const el=$(id);
if(!window.L){
el.innerHTML=`<div class="map-fallback"><div><strong>ไม่สามารถโหลดแผนที่ได้</strong><p>ตรวจสอบอินเทอร์เน็ต หรือกรอก Latitude / Longitude ด้วยตนเอง</p></div></div>`;
return null;
}
const map=L.map(id).setView(center,zoom);
const tiles=L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{
maxZoom:19,attribution:"© OpenStreetMap contributors"
});
let tileErrorCount=0;
tiles.on("tileerror",()=>{
tileErrorCount++;
if(tileErrorCount===3) toast("โหลดแผนที่บางส่วนไม่สำเร็จ แต่ยังกรอกพิกัดด้วยตนเองได้");
});
tiles.addTo(map);
return map;
}
function initDashboardMap(){
if(maps.dashboard)return;
maps.dashboard=createMap("dashboardMap",[13.6678,100.6351],12);
if(!maps.dashboard)return;
records.slice(0,6).forEach(r=>L.marker([r.lat,r.lng],{icon:divIcon(markerColor(r.status))}).addTo(maps.dashboard).bindPopup(`<strong>${r.id}</strong><br>${r.type}<br>${r.address}`));
warnings.slice(0,4).forEach(w=>L.marker([w.lat,w.lng],{icon:divIcon("#C73B32","⚠")}).addTo(maps.dashboard).bindPopup(`<strong>${w.type}</strong><br>${w.description}`));
}
function initMainMap(){
if(maps.main)return;
maps.main=createMap("mainMap",[13.6678,100.6351],12);
if(!maps.main)return;
maps.main.on("click",e=>{
if(manualPinMode){
updateFormPosition(e.latlng.lat,e.latlng.lng,null);
toast("ย้ายหมุดไปยังตำแหน่งที่เลือกแล้ว");
}
});
}
function initFormMap(){
if(maps.form)return;
maps.form=createMap("formMap",[13.6678,100.6351],15);
if(!maps.form)return;
formPin=L.marker([13.6678,100.6351],{draggable:true,icon:divIcon("#FFD91E")}).addTo(maps.form);
formPin.on("dragend",e=>{
const p=e.target.getLatLng();
updateFormPosition(p.lat,p.lng,null,false);
});
maps.form.on("click",e=>{
if(manualPinMode) updateFormPosition(e.latlng.lat,e.latlng.lng,null);
});
}
function initSafetyMap(){
if(maps.safety)return;
maps.safety=createMap("safetyMap",[13.6678,100.6351],12);
renderSafetyMapMarkers();
}
function initNearbyMap(){
if(maps.nearby)return;
maps.nearby=createMap("nearbyMap",[13.6678,100.6351],14);
renderNearbyMapMarkers();
}
function renderMapMarkers(filtered=records){
if(!maps.main)return;
if(layers.mainGroup) layers.mainGroup.clearLayers();
layers.mainGroup=L.layerGroup().addTo(maps.main);
filtered.forEach(r=>{
const marker=L.marker([r.lat,r.lng],{icon:divIcon(markerColor(r.status))})
.addTo(layers.mainGroup)
.on("click",()=>showMapRecord(r));
marker.bindTooltip(`${r.id} · ${r.type}`);
});
warnings.filter(w=>w.active).forEach(w=>{
L.marker([w.lat,w.lng],{icon:divIcon(w.severity==="เสี่ยงสูง"?"#C73B32":"#E67E22","⚠")})
.addTo(layers.mainGroup).on("click",()=>showMapWarning(w));
});
const placeFilter=$("mapPlaceFilter")?.value;
if(placeFilter){
places.filter(p=>p.category===placeFilter).forEach(p=>{
L.marker([p.lat,p.lng],{icon:divIcon("#2D6CDF","●")}).addTo(layers.mainGroup).on("click",()=>showMapPlace(p));
});
}
if(currentPosition){
L.circleMarker([currentPosition.lat,currentPosition.lng],{radius:9,color:"#2D6CDF",fillColor:"#2D6CDF",fillOpacity:.7}).addTo(layers.mainGroup).bindTooltip("ตำแหน่งปัจจุบัน");
}
}
function renderSafetyMapMarkers(){
if(!maps.safety)return;
if(layers.safetyGroup)layers.safetyGroup.clearLayers();
layers.safetyGroup=L.layerGroup().addTo(maps.safety);
warnings.forEach(w=>{
L.marker([w.lat,w.lng],{icon:divIcon(w.active?(w.severity==="เสี่ยงสูง"?"#C73B32":"#E67E22"):"#746D6A","⚠")})
.addTo(layers.safetyGroup).bindPopup(`<strong>${w.type}</strong><br>${w.description}<br>${w.active?"ยังมีอยู่":"แก้ไขแล้ว"}`);
});
if(currentPosition) L.circleMarker([currentPosition.lat,currentPosition.lng],{radius:9,color:"#2D6CDF"}).addTo(layers.safetyGroup);
}
function renderNearbyMapMarkers(){
if(!maps.nearby)return;
if(layers.nearbyGroup)layers.nearbyGroup.clearLayers();
layers.nearbyGroup=L.layerGroup().addTo(maps.nearby);
const list=selectedNearbyCategory?places.filter(p=>p.category===selectedNearbyCategory):places;
list.forEach(p=>L.marker([p.lat,p.lng],{icon:divIcon("#2D6CDF","●")}).addTo(layers.nearbyGroup).bindPopup(`<strong>${p.name}</strong><br>${p.category}<br>${p.distance} เมตร`));
if(currentPosition)L.circleMarker([currentPosition.lat,currentPosition.lng],{radius:9,color:"#2D6CDF"}).addTo(layers.nearbyGroup);
}
function filterMapRecords(){
const q=$("mapSearchInput").value.trim().toLowerCase();
const type=$("mapPropertyFilter").value;
const status=$("mapStatusFilter").value;
const warning=$("mapWarningFilter").value;
const filtered=records.filter(r=>{
const text=[r.id,r.address,r.phone,r.project,r.titleDeed,r.type,r.notes].join(" ").toLowerCase();
return (!q||text.includes(q))&&(!type||r.type===type)&&(!status||r.status===status)&&(!warning||r.warnings.includes(warning));
});
renderMapMarkers(filtered);
toast(`พบ ${filtered.length} รายการบนแผนที่`);
}
function recenterMap(){
if(!currentPosition){requestLocationWithExplanation();return}
maps.main?.setView([currentPosition.lat,currentPosition.lng],16);
}
function fitAllMarkers(){
if(!maps.main||!layers.mainGroup)return;
const bounds=layers.mainGroup.getBounds?.();
if(bounds?.isValid()) maps.main.fitBounds(bounds.pad(.12));
}
function showMapRecord(r){
const dist=currentPosition?Math.round(distanceMeters(currentPosition.lat,currentPosition.lng,r.lat,r.lng)):"—";
$("mapSidePanel").innerHTML=`
<img class="full" style="height:160px;object-fit:cover;border-radius:14px" src="${r.images[0]}" alt="">
<div class="spread" style="margin-top:12px"><strong>${r.id}</strong><span class="chip">${r.status}</span></div>
<h3 style="margin-top:10px">${r.type}</h3>
<p class="muted small">${r.address}</p>
<div class="stack small">
<div class="spread"><span>พิกัด</span><strong>${r.lat.toFixed(5)}, ${r.lng.toFixed(5)}</strong></div>
<div class="spread"><span>ระยะจากคุณ</span><strong>${dist} เมตร</strong></div>
<div class="spread"><span>สำรวจล่าสุด</span><strong>${r.updated}</strong></div>
<div class="spread"><span>ผู้บันทึก</span><strong>${r.createdBy}</strong></div>
<div class="spread"><span>ราคาที่ประกาศ</span><strong>${r.price||"ไม่ได้บันทึก"}</strong></div>
<div class="spread"><span>ที่จอดรถ</span><strong>${r.parking}</strong></div>
</div>
<div class="meta">${r.warnings.map(w=>`<span class="chip red">⚠ ${w}</span>`).join("")||'<span class="chip green">ไม่มีจุดเตือน</span>'}</div>
<div class="grid grid-2" style="margin-top:14px">
<button class="btn btn-primary" onclick="openRecord('${r.id}')">ดูรายละเอียด</button>
<button class="btn" onclick="navigateTo(${r.lat},${r.lng})">นำทาง</button>
<button class="btn" onclick="updateExisting('${r.id}')">อัปเดตข้อมูล</button>
<button class="btn" onclick="assignResurvey('${r.id}')">มอบหมายสำรวจซ้ำ</button>
</div>`;
}
function showMapWarning(w){
$("mapSidePanel").innerHTML=`
<img class="full" style="height:160px;object-fit:cover;border-radius:14px" src="${w.image}" alt="">
<div class="spread" style="margin-top:12px"><strong>${w.type}</strong><span class="chip ${w.severity==="เสี่ยงสูง"?"red":"orange"}">${w.severity}</span></div>
<p>${w.description}</p>
<div class="stack small">
<div class="spread"><span>วันที่รายงาน</span><strong>${w.date}</strong></div>
<div class="spread"><span>ผู้รายงาน</span><strong>${w.reporter}</strong></div>
<div class="spread"><span>สถานะ</span><strong>${w.active?"ยังมีอยู่":"แก้ไขแล้ว"}</strong></div>
<div class="spread"><span>ยืนยัน</span><strong>${w.confirm} ครั้ง</strong></div>
</div>
<div class="grid grid-2" style="margin-top:14px">
<button class="btn btn-primary" onclick="confirmWarning('${w.id}')">ยืนยันจุดเตือน</button>
<button class="btn" onclick="resolveWarning('${w.id}')">แจ้งว่าแก้ไขแล้ว</button>
</div>`;
}
function showMapPlace(p){
$("mapSidePanel").innerHTML=`
<div class="chip blue">ข้อมูลตัวอย่าง</div>
<h3 style="margin-top:12px">${p.name}</h3>
<p class="muted">${p.category} · ${p.distance} เมตร · ${p.time}</p>
<p>${p.description}</p>
<div class="grid grid-2">
<button class="btn btn-primary" onclick="navigateTo(${p.lat},${p.lng})">นำทาง</button>
<button class="btn" onclick="toast('บันทึกสถานที่แล้ว')">บันทึกไว้</button>
</div>`;
}
// ---------- GEOLOCATION ----------
// Browser Geolocation จะถูกเรียกหลังผู้ใช้กดอนุญาตเท่านั้น
function requestLocationWithExplanation(){openModal("locationPermissionModal")}
function getCurrentLocation(){
closeModal("locationPermissionModal");
if(!window.isSecureContext && location.protocol!=="file:"){
showLocationError("ต้องเปิดผ่าน HTTPS หรือ localhost จึงจะใช้ตำแหน่งได้");
return;
}
if(!navigator.geolocation){
showLocationError("Browser นี้ไม่รองรับการระบุตำแหน่ง กรุณากรอกพิกัดด้วยตนเอง");
return;
}
navigator.geolocation.getCurrentPosition(
pos=>handlePosition(pos),
err=>handleLocationError(err),
{enableHighAccuracy:true,timeout:12000,maximumAge:0}
);
}
function handlePosition(pos){
currentPosition={lat:pos.coords.latitude,lng:pos.coords.longitude,accuracy:pos.coords.accuracy,updated:new Date()};
updateLocationUI();
updateFormPosition(currentPosition.lat,currentPosition.lng,currentPosition.accuracy);
renderMapMarkers();
renderSafetyMapMarkers();
renderNearbyMapMarkers();
checkProximityWarnings();
closeModal("locationPermissionModal");
toast("ใช้ตำแหน่งปัจจุบันแล้ว");
}
function updateLocationUI(){
if(!currentPosition)return;
$("gpsChip").textContent=locationWatchId!==null?"GPS: ติดตามแบบเรียลไทม์":"GPS: เชื่อมต่อแล้ว";
$("gpsChip").className="chip "+(locationWatchId!==null?"green":"blue");
$("accuracyChip").textContent=`ความแม่นยำ: ±${Math.round(currentPosition.accuracy)} เมตร`;
$("updateChip").textContent=`อัปเดตล่าสุด: ${currentPosition.updated.toLocaleTimeString("th-TH",{hour:"2-digit",minute:"2-digit",second:"2-digit"})}`;
$("locationSummary").textContent=`พิกัด ${currentPosition.lat.toFixed(5)}, ${currentPosition.lng.toFixed(5)} · ±${Math.round(currentPosition.accuracy)} ม.`;
if(currentPosition.accuracy>80) toast("ความแม่นยำ GPS ค่อนข้างต่ำ กรุณาตรวจสอบหมุดก่อนบันทึก");
}
function handleLocationError(err){
const msgs={1:"ไม่ได้รับอนุญาตให้ใช้ตำแหน่ง กรุณากรอกที่อยู่หรือพิกัดด้วยตนเอง",2:"ไม่สามารถระบุตำแหน่งได้ กรุณาตรวจสอบ GPS",3:"ใช้เวลาระบุตำแหน่งนานเกินไป กรุณาลองใหม่"};
showLocationError(msgs[err.code]||"เกิดข้อผิดพลาดในการระบุตำแหน่ง");
}
function showLocationError(message){
toast(message);
$("existingCheckInfo").className="alert warning";
$("existingCheckInfo").innerHTML=`${message}<br><span class="small">คุณยังสามารถกรอกที่อยู่ Latitude และ Longitude หรือปักหมุดด้วยตนเองได้</span>`;
}
function toggleTracking(){
if(locationWatchId!==null){stopTracking();return}
if(!navigator.geolocation){showLocationError("Browser นี้ไม่รองรับการติดตามตำแหน่ง");return}
locationWatchId=navigator.geolocation.watchPosition(
handlePosition,
handleLocationError,
{enableHighAccuracy:true,timeout:15000,maximumAge:2000}
);
$("trackingBtn").textContent="■ หยุดติดตาม";
$("trackingBtn").classList.add("btn-danger");
toast("เริ่มติดตามตำแหน่งแบบเรียลไทม์แล้ว");
}
function stopTracking(){
if(locationWatchId!==null) navigator.geolocation.clearWatch(locationWatchId);
locationWatchId=null;
if($("trackingBtn")){
$("trackingBtn").textContent="▶ เริ่มติดตาม";
$("trackingBtn").classList.remove("btn-danger");
}
updateLocationUI();
toast("หยุดติดตามตำแหน่งแล้ว");
}
function updateFormPosition(lat,lng,accuracy=null,moveMap=true){
$("formLat").value=lat.toFixed(7);
$("formLng").value=lng.toFixed(7);
$("formAccuracy").value=accuracy?`±${Math.round(accuracy)} เมตร`:"ปักหมุดด้วยตนเอง";
if(formPin)formPin.setLatLng([lat,lng]);
if(moveMap)maps.form?.setView([lat,lng],16);
}
function enableManualPin(){
manualPinMode=true;
toast("คลิกแผนที่หรือจับหมุดลากไปยังตำแหน่งที่ต้องการ");
}
function checkProximityWarnings(){
if(!currentPosition)return;
const near=warnings.filter(w=>w.active).map(w=>({...w,realDistance:distanceMeters(currentPosition.lat,currentPosition.lng,w.lat,w.lng)})).sort((a,b)=>a.realDistance-b.realDistance)[0];
if(near&&near.realDistance<300){
$("proximityAlert").classList.remove("hidden");
$("proximityAlert").innerHTML=`⚠ คุณอยู่ห่างจากจุดที่มีรายงาน<strong>${near.type}</strong>ประมาณ ${Math.round(near.realDistance)} เมตร — ${near.description}`;
}else $("proximityAlert").classList.add("hidden");
}
function searchNearMe(){
if(!currentPosition){requestLocationWithExplanation();return}
openPage("search");
setTimeout(()=>{
const near=records.filter(r=>distanceMeters(currentPosition.lat,currentPosition.lng,r.lat,r.lng)<=250);
$("centralSearchInput").value="";
renderSearchResults(near);
toast(`พบข้อมูล ${near.length} รายการภายใน 250 เมตร`);
},150);
}
// ---------- CAMERA ----------
// ใช้ getUserMedia สำหรับ preview จริง และหยุด track ทุกครั้งเมื่อปิด modal
function openCameraPermissionModal(){openModal("cameraPermissionModal")}
async function startCamera(){
closeModal("cameraPermissionModal");
openModal("cameraModal");
$("cameraError").classList.add("hidden");
$("cameraVideo").classList.remove("hidden");
if(!window.isSecureContext && location.protocol!=="file:"){
showCameraError("ต้องเปิดผ่าน HTTPS หรือ localhost เพื่อใช้กล้อง");
return;
}
if(!navigator.mediaDevices?.getUserMedia){
showCameraError("Browser นี้ไม่รองรับกล้อง กรุณาใช้อัปโหลดรูปแทน");
return;
}
try{
stopCameraTracks();
cameraStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:cameraFacingMode}},audio:false});
$("cameraVideo").srcObject=cameraStream;
}catch(err){
const name=err.name||"";
let msg="ไม่สามารถเปิดกล้องได้";
if(name==="NotAllowedError")msg="ไม่ได้รับอนุญาตให้ใช้กล้อง กรุณาอัปโหลดรูปแทน";
if(name==="NotFoundError")msg="ไม่พบกล้องในอุปกรณ์นี้";
if(name==="NotReadableError")msg="กล้องอาจถูกใช้งานโดยแอปอื่น";
if(name==="OverconstrainedError")msg="ไม่พบกล้องตามรูปแบบที่เลือก";
showCameraError(msg);
}
}
function showCameraError(msg){
$("cameraVideo").classList.add("hidden");
$("cameraError").classList.remove("hidden");
$("cameraError").innerHTML=`<strong>${msg}</strong><p class="muted" style="color:#ccc">คุณยังสามารถเลือก “อัปโหลดแทน” ได้</p>`;
}
function stopCameraTracks(){
if(cameraStream)cameraStream.getTracks().forEach(t=>t.stop());
cameraStream=null;
$("cameraVideo").srcObject=null;
}
function closeCamera(){stopCameraTracks();closeModal("cameraModal")}
async function switchCamera(){
cameraFacingMode=cameraFacingMode==="environment"?"user":"environment";
await startCamera();
}
function capturePhoto(){
const video=$("cameraVideo");
if(!cameraStream||!video.videoWidth){toast("กล้องยังไม่พร้อม");return}
const canvas=$("cameraCanvas");
canvas.width=video.videoWidth;canvas.height=video.videoHeight;
canvas.getContext("2d").drawImage(video,0,0);
const data=canvas.toDataURL("image/jpeg",.88);
capturedPhotos.push({src:data,category:$("photoCategory").value,cover:capturedPhotos.length===0});
renderPhotos();
closeCamera();
toast("ถ่ายภาพแล้ว กรุณาตรวจสอบก่อนบันทึก");
}
function handleGalleryUpload(event){
Array.from(event.target.files).forEach(file=>{
const reader=new FileReader();
reader.onload=e=>{capturedPhotos.push({src:e.target.result,category:$("photoCategory").value,cover:capturedPhotos.length===0});renderPhotos()};
reader.readAsDataURL(file);
});
event.target.value="";
}
function handleCameraFallback(event){handleGalleryUpload(event);closeCamera()}
function renderPhotos(){
$("photoGrid").innerHTML=capturedPhotos.length?capturedPhotos.map((p,i)=>`
<div class="photo-item" draggable="true">
<img src="${p.src}" alt="${p.category}">
${p.cover?'<span class="cover">ภาพปก</span>':""}
<button onclick="removePhoto(${i})">✕</button>
<div style="padding:8px;font-size:12px;background:#fff">${p.category}<br><button class="btn btn-sm" style="margin-top:5px" onclick="setCover(${i})">ตั้งเป็นภาพปก</button></div>
</div>`).join(""):`<div class="alert info" style="grid-column:1/-1">ยังไม่มีรูปภาพ คุณสามารถเปิดกล้องหรืออัปโหลดจากอุปกรณ์ได้</div>`;
}
function removePhoto(i){
if(!confirm("ต้องการลบรูปนี้หรือไม่?"))return;
const wasCover=capturedPhotos[i]?.cover;
capturedPhotos.splice(i,1);
if(wasCover&&capturedPhotos[0])capturedPhotos[0].cover=true;
renderPhotos();
}
function setCover(i){capturedPhotos.forEach((p,idx)=>p.cover=idx===i);renderPhotos();toast("ตั้งภาพปกแล้ว")}
function runMockOCR(){
if(!capturedPhotos.length){toast("กรุณาเพิ่มรูปป้ายก่อนใช้ OCR");return}
$("ocrFields").classList.remove("hidden");
$("ocrPhone").value="0892345678";
$("ocrPrice").value="7,900,000 บาท";
$("ocrArea").value="58 ตร.ว.";
$("ocrContact").value="คุณเอ";
toast("อ่านข้อมูลตัวอย่างจากภาพแล้ว กรุณาตรวจสอบและแก้ไขก่อนยืนยัน");
}
// ---------- SEARCH ----------
function renderSearchResults(forced=null){
$("searchLoading").classList.remove("hidden");
setTimeout(()=>{
const q=$("centralSearchInput")?.value.trim().toLowerCase()||"";
const type=$("searchType")?.value||"";
const area=$("searchArea")?.value||"";
const status=$("searchStatus")?.value||"";
const officer=$("searchOfficer")?.value||"";
let list=forced||records.filter(r=>{
const text=[r.id,r.type,r.address,r.titleDeed,r.phone,r.contact,r.project,r.createdBy,r.editedBy,r.notes,r.warnings.join(" ")].join(" ").toLowerCase();
return (!q||text.includes(q))&&(!type||r.type===type)&&(!area||r.area===area)&&(!status||r.status===status)&&(!officer||r.createdBy===officer);
});
const sort=$("searchSort")?.value||"latest";
if(sort==="latest")list.sort((a,b)=>b.updated.localeCompare(a.updated));
if(sort==="oldest")list.sort((a,b)=>a.updated.localeCompare(b.updated));
if(sort==="nearest"&¤tPosition)list.sort((a,b)=>distanceMeters(currentPosition.lat,currentPosition.lng,a.lat,a.lng)-distanceMeters(currentPosition.lat,currentPosition.lng,b.lat,b.lng));
const per=6,totalPages=Math.max(1,Math.ceil(list.length/per));
currentPage=Math.min(currentPage,totalPages);
const pageList=list.slice((currentPage-1)*per,currentPage*per);
$("searchResultCount").textContent=list.length;
$("paginationLabel").textContent=`หน้า ${currentPage} / ${totalPages}`;
$("searchResults").innerHTML=pageList.length?pageList.map(recordCard).join(""):`<div class="alert warning">ไม่พบข้อมูลที่ตรงกับการค้นหา ลองปรับคำค้นหรือเงื่อนไข</div>`;
$("searchLoading").classList.add("hidden");
},180);
}
function recordCard(r){
const dist=currentPosition?`${Math.round(distanceMeters(currentPosition.lat,currentPosition.lng,r.lat,r.lng))} ม.`:"—";
return `<div class="list-item record-card">
<img class="thumb" src="${r.images[0]}" alt="">
<div>
<div class="row"><h4>${r.id}</h4><span class="chip">${r.status}</span></div>
<p><strong>${r.type}</strong> · ${r.address}</p>
<p>ระยะ ${dist} · สำรวจ ${r.surveyDate} · อัปเดต ${r.updated}</p>
<div class="meta">
<span class="chip blue">${r.createdBy}</span>
<span class="chip">${maskPhone(r.phone)}</span>
${r.warnings.map(w=>`<span class="chip red">⚠ ${w}</span>`).join("")}
</div>
</div>
<div class="record-actions stack">
<button class="btn btn-sm btn-primary" onclick="openRecord('${r.id}')">ดู