/* global React, I, UI, SSSData */
// ═══ ติดตามงานติดตั้ง (Installation Site Tracker) ═══════════════════════════
// หลังบ้าน: จัดการไซต์ · สถานะ 8 ขั้น · รูปก่อน/ระหว่าง/หลัง · ผู้รับผิดชอบ
// หน้าบ้าน: กด "เผยแพร่" แล้วข้อมูลชุดที่คัดกรองแล้วจะขึ้นเว็บสาธารณะ /track
//
// ⚠️ กติกาข้อมูลสาธารณะ (ห้ามพัง) — คัดกรองที่ trkBuildPublic() ที่เดียวเท่านั้น
//    1. ส่งออกเฉพาะฟิลด์ใน whitelist — ชื่อภายใน/ที่อยู่/เบอร์/ผู้รับผิดชอบ/วันนัด ไม่มีวันหลุด
//    2. สถานะภายใน 8 ค่า ยุบเหลือ 2 ค่า (ติดตั้งแล้ว / ยังไม่ติดตั้ง) — "มีปัญหา" ไม่โผล่
//    3. พิกัดถูกปัดตามระดับที่ตั้งไว้รายไซต์ (แม่นยำ / ~100 ม. / ~1 กม.)
//    4. รูปที่เผยแพร่ต้องกด "อนุมัติ" เท่านั้น และถูกวาดใหม่ผ่าน canvas ทุกครั้ง → EXIF/GPS หายเกลี้ยง
//    5. เขียนลงตาราง public_store คนละใบกับ sss_store — กุญแจหน้าเว็บสาธารณะอ่านข้อมูลภายในไม่ได้
const { useState: useStateTRK, useEffect: useEffectTRK, useMemo: useMemoTRK } = React;

const TRK_SITES = "sss-track-sites";
const TRK_PHOTOS = "sss-track-photos";

function trkGet(key, def) {
  const v = window.lsSyncGet ? window.lsSyncGet(key) : null;
  if (v != null) return v;
  try { return JSON.parse(localStorage.getItem(key) || "null") || def; } catch { return def; }
}
function trkSet(key, val) {
  if (window.lsSyncSet) window.lsSyncSet(key, val);
  else { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} }
  window.dispatchEvent(new Event("sss-track-changed"));
}

// ── สถานะภายใน 8 ขั้น (ลูกค้าไม่เห็นค่าพวกนี้) ────────────────────────────
const TRK_STATUS = {
  not_installed: { label: "ยังไม่ติดตั้ง", emoji: "⚪", color: "#78716c", bg: "#f5f5f4" },
  scheduled:     { label: "นัดหมายแล้ว",  emoji: "📅", color: "#1d4ed8", bg: "#dbeafe" },
  en_route:      { label: "กำลังเดินทาง", emoji: "🚚", color: "#0e7490", bg: "#cffafe" },
  on_site:       { label: "ถึงหน้างาน",   emoji: "📍", color: "#7c3aed", bg: "#ede9fe" },
  installing:    { label: "กำลังติดตั้ง", emoji: "🔧", color: "#b45309", bg: "#fef3c7" },
  installed:     { label: "ติดตั้งแล้ว",  emoji: "✅", color: "#15803d", bg: "#dcfce7" },
  issue:         { label: "เกิดปัญหา",    emoji: "⚠️", color: "#b91c1c", bg: "#fee2e2" },
  cancelled:     { label: "ยกเลิก",       emoji: "✖",  color: "#57534e", bg: "#e7e5e4" },
};
const TRK_STATUS_ORDER = ["not_installed", "scheduled", "en_route", "on_site", "installing", "installed", "issue", "cancelled"];
// สถานะสาธารณะมีแค่ 2 ค่า — ยุบที่นี่ที่เดียว
const trkPublicStatus = (s) => (s === "installed" ? "installed" : "not_installed");

const TRK_PRECISION = {
  exact:       { label: "แม่นยำ (จุดจริง)", digits: 6 },
  approx_100m: { label: "โดยประมาณ ~100 ม.", digits: 3 },
  approx_1km:  { label: "คร่าว ๆ ~1 กม.",    digits: 2 },
};
const TRK_PHOTO_TYPES = {
  before:   "ก่อนติดตั้ง",
  during:   "ระหว่างติดตั้ง",
  after:    "หลังติดตั้ง",
  issue:    "ปัญหาหน้างาน",
  document: "เอกสาร",
};
const TRK_PROVINCES = ["กรุงเทพมหานคร","กระบี่","กาญจนบุรี","กาฬสินธุ์","กำแพงเพชร","ขอนแก่น","จันทบุรี","ฉะเชิงเทรา","ชลบุรี","ชัยนาท","ชัยภูมิ","ชุมพร","เชียงราย","เชียงใหม่","ตรัง","ตราด","ตาก","นครนายก","นครปฐม","นครพนม","นครราชสีมา","นครศรีธรรมราช","นครสวรรค์","นนทบุรี","นราธิวาส","น่าน","บึงกาฬ","บุรีรัมย์","ปทุมธานี","ประจวบคีรีขันธ์","ปราจีนบุรี","ปัตตานี","พระนครศรีอยุธยา","พังงา","พัทลุง","พิจิตร","พิษณุโลก","เพชรบุรี","เพชรบูรณ์","แพร่","พะเยา","ภูเก็ต","มหาสารคาม","มุกดาหาร","แม่ฮ่องสอน","ยโสธร","ยะลา","ร้อยเอ็ด","ระนอง","ระยอง","ราชบุรี","ลพบุรี","ลำปาง","ลำพูน","เลย","ศรีสะเกษ","สกลนคร","สงขลา","สตูล","สมุทรปราการ","สมุทรสงคราม","สมุทรสาคร","สระแก้ว","สระบุรี","สิงห์บุรี","สุโขทัย","สุพรรณบุรี","สุราษฎร์ธานี","สุรินทร์","หนองคาย","หนองบัวลำภู","อ่างทอง","อำนาจเจริญ","อุดรธานี","อุตรดิตถ์","อุทัยธานี","อุบลราชธานี"];

const trkToday = () => new Date().toISOString().slice(0, 10);
const TRK_MON = ["ม.ค.","ก.พ.","มี.ค.","เม.ย.","พ.ค.","มิ.ย.","ก.ค.","ส.ค.","ก.ย.","ต.ค.","พ.ย.","ธ.ค."];
function trkThai(iso) {
  if (!iso) return "—";
  const [y, m, d] = String(iso).slice(0, 10).split("-").map(Number);
  if (!y || !m || !d) return "—";
  return `${d} ${TRK_MON[m - 1]} ${y + 543}`;
}
const trkUid = () => "ST-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);

// ── รูป: ย่อ + วาดใหม่ผ่าน canvas (ขั้นตอนนี้ทำให้ EXIF/พิกัด GPS หายไปในตัว) ──
function trkResize(fileOrUrl, max, quality) {
  return new Promise(resolve => {
    const run = (src) => {
      const img = new Image();
      if (/^https?:/.test(src)) img.crossOrigin = "anonymous";   // รูปจากคลัง (คนละโดเมน) — กัน canvas taint
      img.onload = () => {
        const sc = Math.min(1, max / Math.max(img.width, img.height));
        const c = document.createElement("canvas");
        c.width = Math.max(1, Math.round(img.width * sc));
        c.height = Math.max(1, Math.round(img.height * sc));
        const ctx = c.getContext("2d");
        ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, c.width, c.height);   // กันพื้นโปร่งใสกลายเป็นดำ
        ctx.drawImage(img, 0, 0, c.width, c.height);
        resolve(c.toDataURL("image/jpeg", quality));
      };
      img.onerror = () => resolve(null);
      img.src = src;
    };
    if (typeof fileOrUrl === "string") run(fileOrUrl);
    else { const r = new FileReader(); r.onload = () => run(r.result); r.onerror = () => resolve(null); r.readAsDataURL(fileOrUrl); }
  });
}

// ── ช่องจังหวัดแบบค้นหา — เปิดมาเจอเฉพาะจังหวัดที่เคยใช้ (สั้น เลือกง่าย)
//    พิมพ์เมื่อไรค่อยค้นจากทั้ง 77 จังหวัด เช่น พิมพ์ "ข" → ขอนแก่น ...
function TrkProvinceAuto({ value, onChange, used }) {
  const [open, setOpen] = useStateTRK(false);
  const q = String(value || "").trim();
  const hits = !open ? [] : (q
    ? TRK_PROVINCES.filter(p => p.includes(q)).slice(0, 10)
    : (used || []).slice(0, 10));
  const usedSet = new Set(used || []);
  return (
    <div style={{position: "relative"}}>
      <input className="input" value={value || ""} placeholder="พิมพ์ชื่อจังหวัด เช่น ข → ขอนแก่น"
        onChange={e => { onChange(e.target.value); setOpen(true); }}
        onFocus={() => setOpen(true)}
        onBlur={() => setTimeout(() => setOpen(false), 160)}/>
      {open && hits.length > 0 && (
        <div style={{position: "absolute", top: "calc(100% + 3px)", left: 0, right: 0, zIndex: 70, background: "var(--surface)", border: "1px solid var(--line-strong)", borderRadius: 9, boxShadow: "0 12px 32px rgba(0,0,0,0.22)", maxHeight: 260, overflowY: "auto"}}>
          {!q && <div style={{padding: "5px 10px", fontSize: 10.5, color: "var(--ink-4)", borderBottom: "1px solid var(--line)"}}>จังหวัดที่เคยใช้ — พิมพ์เพื่อค้นหาจังหวัดอื่น</div>}
          {hits.map(p => (
            <div key={p} onMouseDown={e => { e.preventDefault(); onChange(p); setOpen(false); }}
              style={{display: "flex", justifyContent: "space-between", alignItems: "center", padding: "7px 11px", cursor: "pointer", fontSize: 13}}
              onMouseEnter={e => { e.currentTarget.style.background = "var(--surface-2)"; }}
              onMouseLeave={e => { e.currentTarget.style.background = ""; }}>
              <span>{p}</span>
              {q && usedSet.has(p) && <span style={{fontSize: 10, color: "var(--ink-4)"}}>เคยใช้</span>}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// คัดกรองข้อมูลสาธารณะ — จุดเดียวในระบบที่ตัดสินว่าอะไรออกเว็บได้
// ════════════════════════════════════════════════════════════════════════════
function trkFuzz(v, precision) {
  const n = Number(v);
  if (v === "" || v == null || isNaN(n)) return null;
  const d = (TRK_PRECISION[precision] || TRK_PRECISION.exact).digits;
  const p = Math.pow(10, d);
  return Math.round(n * p) / p;
}
function trkBuildPublic(sites, photos) {
  const pub = (sites || []).filter(s => s && s.isPublic && s.status !== "cancelled");
  const out = pub.map(s => {
    const ok = (photos[s.id] || []).filter(p => p.approved);
    return {
      // ── whitelist: มีแค่ 9 ฟิลด์นี้เท่านั้นที่ออกสู่สาธารณะ ──
      code: s.code || "",
      name: s.namePub || "",
      province: s.province || "",
      district: s.district || "",
      status: trkPublicStatus(s.status),
      installedAt: s.status === "installed" ? (s.installedAt || null) : null,
      note: s.notePub || "",
      lat: trkFuzz(s.lat, s.precision),
      lng: trkFuzz(s.lng, s.precision),
      photoCount: ok.length,
      cover: ok.length ? (ok[0].thumb || null) : null,
    };
  });
  const installed = out.filter(s => s.status === "installed").length;
  return {
    updatedAt: new Date().toISOString(),
    company: (window.SSSData && window.SSSData.Company && window.SSSData.Company.name) || "TRIPLES CREATIVE",
    stats: {
      total: out.length,
      installed,
      notInstalled: out.length - installed,
      provinces: [...new Set(out.map(s => s.province).filter(Boolean))].length,
    },
    sites: out.sort((a, b) => String(b.installedAt || "").localeCompare(String(a.installedAt || "")) || a.code.localeCompare(b.code)),
  };
}

// เขียนข้อมูลสาธารณะขึ้นตาราง public_store (คนละใบกับข้อมูลภายใน)
async function trkPublish(sites, photos) {
  const db = window.SSSDB;
  if (!db || !db.url) throw new Error("ยังไม่ได้เชื่อมต่อฐานข้อมูล — รอสักครู่แล้วลองใหม่");
  const H = { apikey: db.key, Authorization: "Bearer " + db.key, "Content-Type": "application/json" };
  const payload = trkBuildPublic(sites, photos);
  const codes = payload.sites.map(s => s.code);

  const rows = [{ key: "track", data: payload, updated_at: new Date().toISOString() }];
  payload.sites.forEach(s => {
    const src = sites.find(x => x.code === s.code);
    const ok = ((photos[src.id] || []).filter(p => p.approved))
      .map(p => ({ url: p.pub || p.url, caption: p.caption || "", type: p.type || "after" }));
    rows.push({ key: "track:photos:" + s.code, data: { photos: ok }, updated_at: new Date().toISOString() });
  });

  // แบ่งก้อนตามขนาดจริง — รูป base64 ทำให้ก้อนใหญ่เร็วมาก
  const chunks = [];
  let cur = [], size = 0;
  rows.forEach(r => {
    const s = JSON.stringify(r).length;
    if (cur.length && size + s > 3 * 1024 * 1024) { chunks.push(cur); cur = []; size = 0; }
    cur.push(r); size += s;
  });
  if (cur.length) chunks.push(cur);

  for (const c of chunks) {
    const res = await fetch(db.url + "/rest/v1/public_store?on_conflict=key", {
      method: "POST",
      headers: { ...H, Prefer: "resolution=merge-duplicates,return=minimal" },
      body: JSON.stringify(c),
    });
    if (!res.ok) {
      const t = (await res.text()).slice(0, 200);
      if (res.status === 404 || /public_store/.test(t)) {
        throw new Error("ยังไม่มีตารางข้อมูลสาธารณะ — รันที่เครื่องฐานข้อมูลก่อน: bash migrate/setup-public-track.sh");
      }
      throw new Error(`เผยแพร่ไม่สำเร็จ (HTTP ${res.status}) ${t}`);
    }
  }

  // เก็บกวาด: ลบรูปของไซต์ที่เลิกเผยแพร่แล้ว ไม่ให้ค้างอยู่บนเว็บ
  try {
    const res = await fetch(db.url + "/rest/v1/public_store?select=key&key=like.track:photos:*", { headers: H });
    if (res.ok) {
      const have = await res.json();
      const stale = have.map(r => r.key).filter(k => !codes.includes(k.replace("track:photos:", "")));
      for (const k of stale) {
        await fetch(db.url + "/rest/v1/public_store?key=eq." + encodeURIComponent(k), { method: "DELETE", headers: H });
      }
    }
  } catch {}

  return payload;
}

// ════════════════════════════════════════════════════════════════════════════
// ฟอร์มเพิ่ม/แก้ไขไซต์
// ════════════════════════════════════════════════════════════════════════════
function TrkSiteModal({ site, onClose, onSaved }) {
  const { Btn, Drawer, CustomerAutocomplete } = window.UI;
  const D = window.SSSData;
  const isNew = !site.id;
  const [f, setF] = useStateTRK(() => ({
    id: site.id || trkUid(),
    code: site.code || "",
    nameInt: site.nameInt || "",
    namePub: site.namePub || "",
    customer: site.customer || "",
    descInt: site.descInt || "",
    notePub: site.notePub || "",
    province: site.province || "",
    district: site.district || "",
    subdistrict: site.subdistrict || "",
    address: site.address || "",
    lat: site.lat == null ? "" : site.lat,
    lng: site.lng == null ? "" : site.lng,
    precision: site.precision || "approx_100m",
    status: site.status || "not_installed",
    scheduledAt: site.scheduledAt || "",
    installedAt: site.installedAt || "",
    assignee: site.assignee || "",
    isPublic: site.isPublic !== undefined ? site.isPublic : true,
  }));
  const upd = (k, v) => setF(p => ({ ...p, [k]: v }));
  const emps = D.Employees || [];
  const [more, setMore] = useStateTRK(false);
  // รูปแนบในฟอร์มเลย — อัพโหลดแล้วถือว่าอนุมัติเผยแพร่ทันที (ผู้ใช้เลือกเองกับมือ)
  const [photos, setPhotos] = useStateTRK(() => (trkGet(TRK_PHOTOS, {}) || {})[site.id] || []);
  const [busy, setBusy] = useStateTRK("");

  const addPhotos = async (e) => {
    const files = [...(e.target.files || [])]; e.target.value = "";
    if (!files.length) return;
    setBusy(`กำลังเตรียมรูป ${files.length} รูป (ย่อ + ลบข้อมูล GPS)…`);
    const add = [];
    for (const fl of files) {
      // เก็บสำเนาเดียว (1280px) + thumbnail — ไม่เก็บต้นฉบับใหญ่ซ้ำ กันฐานข้อมูลบวมจนแอปอืด
      const pub = await trkResize(fl, 1280, 0.8);
      if (!pub) continue;
      const thumb = await trkResize(pub, 320, 0.6);
      add.push({ id: trkUid(), pub, thumb, type: "after", caption: "", approved: true, at: new Date().toISOString() });
    }
    setBusy("");
    if (add.length) setPhotos(p => [...p, ...add]);
  };
  const delPhoto = (id) => setPhotos(p => p.filter(x => x.id !== id));

  const save = () => {
    const name = String(f.nameInt || "").trim();
    if (!name) { window.toast && window.toast("กรอกชื่อไซต์ เช่น SRN8555"); return; }
    // ชื่อแบบ SRN8555 ใช้เป็นรหัสได้เลย · ชื่อไทยระบบตั้งรหัสให้เอง
    let code = String(f.code || "").trim().toUpperCase();
    if (!code) code = /^[A-Za-z0-9][A-Za-z0-9-]{1,31}$/.test(name) ? name.toUpperCase() : ("S" + Date.now().toString(36).toUpperCase());
    const all = trkGet(TRK_SITES, []);
    if (all.some(s => s.code === code && s.id !== f.id)) { window.toast && window.toast(`ชื่อ/รหัส ${code} ซ้ำกับไซต์อื่น`); return; }
    if (f.status === "installed" && !photos.length) { window.toast && window.toast("สถานะ 'ติดตั้งแล้ว' ต้องแนบรูปอย่างน้อย 1 รูป"); return; }
    // พิมพ์จังหวัดค้างครึ่งคำ (เช่น "ขอนแ") — ถ้าเทียบได้จังหวัดเดียว ปรับเป็นชื่อเต็มให้
    let prov = String(f.province || "").trim();
    if (prov && !TRK_PROVINCES.includes(prov)) {
      const hit = TRK_PROVINCES.filter(p => p.includes(prov));
      if (hit.length === 1) prov = hit[0];
    }
    const rec = {
      ...f, code, province: prov,
      nameInt: name,
      namePub: String(f.namePub || name).trim(),
      lat: f.lat === "" ? null : Number(f.lat),
      lng: f.lng === "" ? null : Number(f.lng),
      installedAt: f.status === "installed" ? (f.installedAt || trkToday()) : f.installedAt,
      createdAt: site.createdAt || new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      updatedBy: (window.Session && window.Session.get() && window.Session.get().name) || "",
    };
    const next = isNew ? [rec, ...all] : all.map(s => (s.id === rec.id ? rec : s));
    trkSet(TRK_SITES, next);
    const allPh = trkGet(TRK_PHOTOS, {}) || {};
    allPh[rec.id] = photos;
    trkSet(TRK_PHOTOS, allPh);
    window.logActivity && window.logActivity(isNew ? "เพิ่มไซต์ติดตั้ง" : "แก้ไขไซต์ติดตั้ง", `${rec.code} · ${rec.nameInt}`, isNew ? "create" : "update");
    window.toast && window.toast(`บันทึกไซต์ ${rec.code} แล้ว (${photos.length} รูป)`);
    onSaved && onSaved(next);
    onClose();
  };

  const st = TRK_STATUS[f.status] || TRK_STATUS.not_installed;
  return (
    <Drawer open={true} onClose={onClose} wide title={isNew ? "เพิ่มไซต์ติดตั้ง" : `แก้ไขไซต์ ${f.code || f.nameInt}`}
      footer={<>
        <Btn kind="ghost" onClick={onClose}>ยกเลิก</Btn>
        <Btn kind="primary" icon={I.check} onClick={save}>บันทึก</Btn>
      </>}>
      {/* ── ฟอร์มหลัก: 4 อย่างพอ — ชื่อไซต์ · สถานะ · จังหวัด · รูป ── */}
      <div className="field mb"><label>ชื่อไซต์ *</label>
        <input className="input" style={{fontSize: 16, fontWeight: 600}} value={f.nameInt} autoFocus={isNew}
          onChange={e => upd("nameInt", e.target.value)} placeholder="เช่น SRN8555"/></div>

      <div className="field-row cols-2 mb">
        <div className="field"><label>สถานะ</label>
          <select className="input" value={f.status} onChange={e => upd("status", e.target.value)} style={{color: st.color, fontWeight: 600}}>
            {TRK_STATUS_ORDER.map(k => <option key={k} value={k}>{TRK_STATUS[k].emoji} {TRK_STATUS[k].label}</option>)}
          </select></div>
        <div className="field"><label>จังหวัด</label>
          <TrkProvinceAuto value={f.province} onChange={v => upd("province", v)}
            used={(() => {
              const cnt = {};
              (trkGet(TRK_SITES, []) || []).forEach(s => { if (s.province) cnt[s.province] = (cnt[s.province] || 0) + 1; });
              return Object.keys(cnt).sort((a, b) => cnt[b] - cnt[a]);   // เรียงตามที่ใช้บ่อย
            })()}/></div>
      </div>

      <div className="field mb"><label>รูปภาพหน้างาน ({photos.length} รูป)</label>
        <div style={{display: "flex", gap: 7, flexWrap: "wrap", alignItems: "center"}}>
          {photos.map(p => (
            <div key={p.id} style={{position: "relative"}}>
              <img src={p.thumb || p.pub || p.url} alt="" style={{width: 86, height: 74, objectFit: "cover", borderRadius: 8, border: "1px solid var(--line)", background: "#fff", display: "block"}}/>
              <button className="icon-btn" title="ลบรูปนี้" onClick={() => delPhoto(p.id)}
                style={{position: "absolute", top: -6, right: -6, width: 19, height: 19, background: "var(--danger)", color: "#fff", borderRadius: "50%", padding: 0}}><I.close size={9}/></button>
            </div>
          ))}
          <window.UI.DropZone>
            <label title="อัพโหลดรูป (เลือกได้หลายรูป)" style={{width: 86, height: 74, borderRadius: 8, cursor: "pointer", border: "1.5px dashed var(--line-strong)", display: "grid", placeItems: "center", background: "var(--surface-2)"}}>
              <input type="file" accept="image/*" multiple style={{display: "none"}} onChange={addPhotos}/>
              <span style={{display: "flex", flexDirection: "column", alignItems: "center", gap: 2, color: "var(--ink-3)"}}><I.camera size={17}/><span style={{fontSize: 10}}>เพิ่มรูป</span></span>
            </label>
          </window.UI.DropZone>
        </div>
        {busy && <div style={{fontSize: 12, color: "var(--ink-3)", marginTop: 6}}>⏳ {busy}</div>}
        <div style={{fontSize: 11, color: "var(--ink-4)", marginTop: 5}}>รูปถูกย่อและลบข้อมูล EXIF/พิกัด GPS อัตโนมัติก่อนขึ้นเว็บ</div>
      </div>

      <label style={{display: "flex", alignItems: "center", gap: 9, padding: "10px 12px", border: "1px solid var(--line-strong)", borderRadius: 9, cursor: "pointer", background: f.isPublic ? "rgba(21,128,61,0.08)" : "var(--surface-2)", marginBottom: 10}}>
        <input type="checkbox" checked={f.isPublic} onChange={e => upd("isPublic", e.target.checked)}/>
        <span>
          <b>เผยแพร่ไซต์นี้บนเว็บสาธารณะ</b>
          <div style={{fontSize: 11.5, color: "var(--ink-3)"}}>บันทึกแล้วกดปุ่ม "เผยแพร่ขึ้นเว็บ" ที่หน้ารายการอีกครั้ง ถึงจะขึ้นเว็บจริง</div>
        </span>
      </label>

      {/* ── รายละเอียดเพิ่มเติม (ซ่อนไว้ ไม่บังคับกรอก) ── */}
      <button className="btn" style={{width: "100%", justifyContent: "center", fontSize: 12.5}} onClick={() => setMore(v => !v)}>
        {more ? "▲ ซ่อนรายละเอียดเพิ่มเติม" : "▼ รายละเอียดเพิ่มเติม (ที่อยู่ · พิกัด · ลูกค้า · วันนัด — ไม่บังคับ)"}
      </button>
      {more && (
        <div style={{marginTop: 10}}>
          <div style={{padding: "7px 11px", background: "rgba(180,83,9,0.09)", borderRadius: 8, fontSize: 11.5, marginBottom: 10}}>
            🔒 ช่องที่มีเครื่องหมายนี้ <b>ไม่ออกสู่หน้าเว็บสาธารณะ</b> ใช้ภายในเท่านั้น
          </div>
          <div className="field-row cols-2 mb">
            <div className="field"><label>ชื่อที่แสดงบนเว็บ (ถ้าต่างจากชื่อไซต์)</label>
              <input className="input" value={f.namePub} onChange={e => upd("namePub", e.target.value)} placeholder="เว้นว่าง = ใช้ชื่อไซต์"/></div>
            <div className="field"><label>🔒 ลูกค้า</label>
              {CustomerAutocomplete
                ? <CustomerAutocomplete value={f.customer} onChange={v => upd("customer", v)} placeholder="พิมพ์ชื่อลูกค้า"/>
                : <input className="input" value={f.customer} onChange={e => upd("customer", e.target.value)}/>}
            </div>
          </div>
          <div className="field-row cols-2 mb">
            <div className="field"><label>🔒 ผู้รับผิดชอบ</label>
              <select className="input" value={f.assignee} onChange={e => upd("assignee", e.target.value)}>
                <option value="">— ยังไม่ระบุ —</option>
                {emps.map(e2 => <option key={e2.id} value={e2.name}>{e2.name}{e2.role ? ` (${e2.role})` : ""}</option>)}
              </select></div>
            <div className="field"><label>อำเภอ/เขต</label>
              <input className="input" value={f.district} onChange={e => upd("district", e.target.value)}/></div>
          </div>
          <div className="field mb"><label>🔒 ที่อยู่/จุดสังเกต (ใช้เดินทางไปหน้างาน)</label>
            <input className="input" value={f.address} onChange={e => upd("address", e.target.value)}/></div>
          <div className="field-row" style={{display: "grid", gridTemplateColumns: "1fr 1fr 1.3fr", gap: 8, marginBottom: 10}}>
            <div className="field"><label>🔒 ละติจูด</label>
              <input className="input mono" value={f.lat} onChange={e => upd("lat", e.target.value)} placeholder="13.7563"/></div>
            <div className="field"><label>🔒 ลองจิจูด</label>
              <input className="input mono" value={f.lng} onChange={e => upd("lng", e.target.value)} placeholder="100.5018"/></div>
            <div className="field"><label>ความละเอียดพิกัดบนเว็บ</label>
              <select className="input" value={f.precision} onChange={e => upd("precision", e.target.value)}>
                {Object.entries(TRK_PRECISION).map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
              </select></div>
          </div>
          <div className="field-row cols-2 mb">
            <div className="field"><label>🔒 วันนัดติดตั้ง</label>
              <input className="input" type="date" value={f.scheduledAt} onChange={e => upd("scheduledAt", e.target.value)}/></div>
            <div className="field"><label>วันที่ติดตั้งเสร็จ (แสดงบนเว็บ)</label>
              <input className="input" type="date" value={f.installedAt} onChange={e => upd("installedAt", e.target.value)}/></div>
          </div>
          <div className="field mb"><label>🔒 โน้ตภายใน (เบอร์ติดต่อ/ข้อควรระวัง)</label>
            <textarea className="textarea" style={{minHeight: 48}} value={f.descInt} onChange={e => upd("descInt", e.target.value)}/></div>
          <div className="field"><label>ข้อความบนเว็บสาธารณะ</label>
            <textarea className="textarea" style={{minHeight: 48}} value={f.notePub} onChange={e => upd("notePub", e.target.value)} placeholder="เช่น ติดตั้งราวสแตนเลส 12 เมตร"/></div>
        </div>
      )}
    </Drawer>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// รายละเอียดไซต์ + จัดการรูป
// ════════════════════════════════════════════════════════════════════════════
function TrkDetailDrawer({ site, onClose, onEdit, onChanged }) {
  const { Btn, Drawer, Badge } = window.UI;
  const [photos, setPhotos] = useStateTRK(() => (trkGet(TRK_PHOTOS, {}) || {})[site.id] || []);
  const [busy, setBusy] = useStateTRK("");
  const st = TRK_STATUS[site.status] || TRK_STATUS.not_installed;

  const savePhotos = (list) => {
    const all = trkGet(TRK_PHOTOS, {}) || {};
    all[site.id] = list;
    trkSet(TRK_PHOTOS, all);
    setPhotos(list);
    onChanged && onChanged();
  };

  const addPhotos = async (e) => {
    const files = [...(e.target.files || [])]; e.target.value = "";
    if (!files.length) return;
    setBusy(`กำลังย่อรูป ${files.length} รูป…`);
    const add = [];
    for (const fl of files) {
      // สำเนาเดียว 1280px (วาดผ่าน canvas = EXIF/GPS หายแล้ว) — ตอนอนุมัติไม่ต้องสร้างซ้ำ
      const pub = await trkResize(fl, 1280, 0.8);
      if (!pub) continue;
      const thumb = await trkResize(pub, 320, 0.6);
      add.push({ id: trkUid(), pub, thumb, type: "after", caption: "", approved: false, at: new Date().toISOString() });
    }
    setBusy("");
    if (add.length) savePhotos([...photos, ...add]);
  };

  // อนุมัติเผยแพร่ — รูปถูกวาดใหม่+ย่อไว้ตั้งแต่ตอนอัพโหลดแล้ว แค่สลับสถานะ (รูปเก่าที่ยังไม่มี pub/thumb สร้างให้)
  const approve = async (p) => {
    let { pub, thumb } = p;
    if (!pub || !thumb) {
      setBusy("กำลังเตรียมรูปสำหรับเผยแพร่ (ลบข้อมูล EXIF/GPS)…");
      pub = pub || await trkResize(p.url || p.pub, 1280, 0.8);
      thumb = thumb || await trkResize(pub, 320, 0.6);
      setBusy("");
      if (!pub) { window.toast && window.toast("เตรียมรูปไม่สำเร็จ"); return; }
    }
    savePhotos(photos.map(x => (x.id === p.id ? { ...x, approved: true, pub, thumb, url: undefined } : x)));
    window.toast && window.toast("อนุมัติรูปแล้ว — จะขึ้นเว็บเมื่อกดเผยแพร่");
  };
  // ถอนเผยแพร่ = สลับสถานะเฉย ๆ เก็บรูปไว้ (เดิมลบ pub/thumb ทิ้งทำให้รูปหายถ้าไม่มีต้นฉบับ)
  const unapprove = (p) => savePhotos(photos.map(x => (x.id === p.id ? { ...x, approved: false } : x)));
  const delPhoto = async (p) => {
    const ok = window.confirmDelete ? await window.confirmDelete("รูปนี้", TRK_PHOTO_TYPES[p.type] || "") : window.confirm("ลบรูปนี้?");
    if (ok) savePhotos(photos.filter(x => x.id !== p.id));
  };
  const updPhoto = (p, k, v) => savePhotos(photos.map(x => (x.id === p.id ? { ...x, [k]: v } : x)));

  const setStatus = (s) => {
    if (s === "installed" && !photos.some(p => p.type === "after")) {
      window.toast && window.toast("ต้องมีรูป 'หลังติดตั้ง' อย่างน้อย 1 รูปก่อนตั้งสถานะติดตั้งแล้ว");
      return;
    }
    const all = trkGet(TRK_SITES, []);
    const next = all.map(x => (x.id === site.id
      ? { ...x, status: s, installedAt: s === "installed" ? (x.installedAt || trkToday()) : x.installedAt, updatedAt: new Date().toISOString() }
      : x));
    trkSet(TRK_SITES, next);
    window.logActivity && window.logActivity("เปลี่ยนสถานะไซต์", `${site.code} → ${TRK_STATUS[s].label}`, "update");
    onChanged && onChanged();
  };

  const approved = photos.filter(p => p.approved).length;

  return (
    <Drawer open={true} onClose={onClose} wide title={`ไซต์ ${site.code}`}
      footer={<>
        <Btn kind="ghost" onClick={onClose}>ปิด</Btn>
        <Btn icon={I.edit} onClick={() => { onClose(); onEdit(site); }}>แก้ไขข้อมูลไซต์</Btn>
      </>}>
      <div style={{padding: "11px 13px", borderRadius: 10, background: st.bg, border: "1px solid var(--line)", marginBottom: 12}}>
        <div style={{display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap"}}>
          <div style={{flex: 1, minWidth: 0}}>
            <div style={{fontSize: 14.5, fontWeight: 700}}>{site.nameInt}</div>
            <div style={{fontSize: 12, color: "var(--ink-3)"}}>
              {[site.district, site.province].filter(Boolean).join(" · ") || "—"}
              {site.customer ? ` · ลูกค้า ${site.customer}` : ""}
            </div>
          </div>
          <div style={{fontSize: 13, fontWeight: 700, color: st.color}}>{st.emoji} {st.label}</div>
        </div>
      </div>

      <div className="label mb">เปลี่ยนสถานะ</div>
      <div style={{display: "flex", flexWrap: "wrap", gap: 5, marginBottom: 14}}>
        {TRK_STATUS_ORDER.map(k => (
          <button key={k} className="btn" onClick={() => setStatus(k)}
            style={{padding: "4px 10px", height: 28, fontSize: 11.5,
              background: site.status === k ? TRK_STATUS[k].color : undefined,
              color: site.status === k ? "#fff" : undefined,
              borderColor: site.status === k ? "transparent" : undefined}}>
            {TRK_STATUS[k].emoji} {TRK_STATUS[k].label}
          </button>
        ))}
      </div>

      <div style={{display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 14, fontSize: 12}}>
        <div style={{border: "1px solid var(--line)", borderRadius: 8, padding: "7px 11px"}}>
          <div style={{fontSize: 10.5, color: "var(--ink-3)"}}>🔒 วันนัดติดตั้ง</div>
          <div style={{fontWeight: 600}}>{trkThai(site.scheduledAt)}</div>
        </div>
        <div style={{border: "1px solid var(--line)", borderRadius: 8, padding: "7px 11px"}}>
          <div style={{fontSize: 10.5, color: "var(--ink-3)"}}>วันที่ติดตั้งเสร็จ</div>
          <div style={{fontWeight: 600}}>{trkThai(site.installedAt)}</div>
        </div>
        {site.address && <div style={{gridColumn: "1 / -1", border: "1px solid var(--line)", borderRadius: 8, padding: "7px 11px"}}>
          <div style={{fontSize: 10.5, color: "var(--ink-3)"}}>🔒 ที่อยู่</div>
          <div>{site.address}</div>
          {site.lat != null && site.lng != null && <a style={{fontSize: 11.5}} href={`https://www.google.com/maps?q=${site.lat},${site.lng}`} target="_blank" rel="noreferrer">นำทางด้วย Google Maps →</a>}
        </div>}
        {site.descInt && <div style={{gridColumn: "1 / -1", border: "1px solid var(--line)", borderRadius: 8, padding: "7px 11px"}}>
          <div style={{fontSize: 10.5, color: "var(--ink-3)"}}>🔒 รายละเอียดภายใน</div>
          <div style={{whiteSpace: "pre-wrap"}}>{site.descInt}</div>
        </div>}
      </div>

      <div className="label mb" style={{display: "flex", alignItems: "center", justifyContent: "space-between"}}>
        <span>รูปหน้างาน ({photos.length} รูป · อนุมัติเผยแพร่แล้ว {approved})</span>
        <window.UI.DropZone>
          <label className="btn" style={{cursor: "pointer", height: 28, padding: "0 10px", fontSize: 11.5}}>
            <input type="file" accept="image/*" multiple style={{display: "none"}} onChange={addPhotos}/>
            <I.camera size={13}/> เพิ่มรูป
          </label>
        </window.UI.DropZone>
      </div>
      {busy && <div style={{padding: 9, background: "var(--surface-2)", borderRadius: 8, fontSize: 12, marginBottom: 8}}>⏳ {busy}</div>}
      {photos.length === 0
        ? <div style={{padding: 16, textAlign: "center", color: "var(--ink-3)", fontSize: 12.5, background: "var(--surface-2)", borderRadius: 9}}>ยังไม่มีรูป — ต้องมีรูป "หลังติดตั้ง" อย่างน้อย 1 รูปจึงจะตั้งสถานะติดตั้งแล้วได้</div>
        : <div style={{display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))", gap: 9}}>
            {photos.map(p => (
              <div key={p.id} style={{border: "1px solid " + (p.approved ? "var(--success)" : "var(--line)"), borderRadius: 9, overflow: "hidden", background: "var(--surface-2)"}}>
                <img src={p.pub || p.url} alt="" style={{width: "100%", height: 120, objectFit: "cover", display: "block", background: "#fff"}}/>
                <div style={{padding: 7, display: "flex", flexDirection: "column", gap: 5}}>
                  <select className="input" style={{height: 26, fontSize: 11.5}} value={p.type} onChange={e => updPhoto(p, "type", e.target.value)}>
                    {Object.entries(TRK_PHOTO_TYPES).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
                  </select>
                  <input className="input" style={{height: 26, fontSize: 11.5}} placeholder="คำบรรยาย (แสดงบนเว็บ)" value={p.caption || ""} onChange={e => updPhoto(p, "caption", e.target.value)}/>
                  <div style={{display: "flex", gap: 5, alignItems: "center"}}>
                    {p.approved
                      ? <Btn size="sm" kind="ghost" onClick={() => unapprove(p)} style={{flex: 1}}>✓ เผยแพร่อยู่ — กดเพื่อถอน</Btn>
                      : <Btn size="sm" kind="primary" onClick={() => approve(p)} style={{flex: 1}}>อนุมัติเผยแพร่</Btn>}
                    <button className="icon-btn" title="ลบรูป" onClick={() => delPhoto(p)}><I.trash size={12}/></button>
                  </div>
                </div>
              </div>
            ))}
          </div>}
      <div style={{marginTop: 10, fontSize: 11.5, color: "var(--ink-3)", lineHeight: 1.7}}>
        รูปที่กด "อนุมัติเผยแพร่" จะถูกวาดใหม่และย่อขนาดก่อนขึ้นเว็บ — <b>ข้อมูล EXIF และพิกัด GPS ในไฟล์รูปจะถูกลบทิ้งทั้งหมด</b> ต้นฉบับยังเก็บไว้ในระบบตามเดิม
      </div>
    </Drawer>
  );
}

// ════════════════════════════════════════════════════════════════════════════
// หน้าหลัก
// ════════════════════════════════════════════════════════════════════════════
function SiteTrackerPage({ navigate }) {
  const { Btn, Badge, Stat, Card, PageHead, fmt0 } = window.UI;
  const [, force] = useStateTRK(0);
  const [q, setQ] = useStateTRK("");
  const [fStatus, setFStatus] = useStateTRK("");
  const [fProv, setFProv] = useStateTRK("");
  const [onlyPub, setOnlyPub] = useStateTRK(false);
  const [edit, setEdit] = useStateTRK(null);
  const [detail, setDetail] = useStateTRK(null);
  const [publishing, setPublishing] = useStateTRK(false);

  useEffectTRK(() => {
    const h = () => force(n => n + 1);
    ["sss-track-changed", "sss-db-loaded", "sss-localstore-polled"].forEach(e => window.addEventListener(e, h));
    return () => ["sss-track-changed", "sss-db-loaded", "sss-localstore-polled"].forEach(e => window.removeEventListener(e, h));
  }, []);

  const sites = trkGet(TRK_SITES, []) || [];
  const photos = trkGet(TRK_PHOTOS, {}) || {};
  const provinces = useMemoTRK(() => [...new Set(sites.map(s => s.province).filter(Boolean))].sort(), [sites.length, JSON.stringify(sites.map(s => s.province))]);

  const shown = sites.filter(s => {
    if (fStatus && s.status !== fStatus) return false;
    if (fProv && s.province !== fProv) return false;
    if (onlyPub && !s.isPublic) return false;
    const sq = q.trim().toLowerCase();
    if (!sq) return true;
    return [s.code, s.nameInt, s.namePub, s.customer, s.province, s.district, s.assignee]
      .some(v => String(v || "").toLowerCase().includes(sq));
  });

  const cnt = (k) => sites.filter(s => s.status === k).length;
  const pubCount = sites.filter(s => s.isPublic).length;
  const installed = cnt("installed");

  const doPublish = async () => {
    if (!pubCount) { window.toast && window.toast('ยังไม่มีไซต์ที่ติ๊ก "เผยแพร่" — เปิดไซต์แล้วติ๊กก่อน'); return; }
    setPublishing(true);
    try {
      const payload = await trkPublish(sites, photos);
      window.logActivity && window.logActivity("เผยแพร่ข้อมูลติดตั้งขึ้นเว็บ", `${payload.sites.length} ไซต์`, "update");
      window.toast && window.toast(`เผยแพร่ขึ้นเว็บแล้ว ${payload.sites.length} ไซต์ · ติดตั้งแล้ว ${payload.stats.installed}`);
    } catch (e) {
      window.toast && window.toast("⚠️ " + (e.message || e));
    }
    setPublishing(false);
  };

  const remove = async (s) => {
    const ok = window.confirmDelete ? await window.confirmDelete(`ไซต์ ${s.code}`, s.nameInt) : window.confirm("ลบไซต์นี้?");
    if (!ok) return;
    trkSet(TRK_SITES, sites.filter(x => x.id !== s.id));
    const all = trkGet(TRK_PHOTOS, {}) || {}; delete all[s.id]; trkSet(TRK_PHOTOS, all);
    window.logActivity && window.logActivity("ลบไซต์ติดตั้ง", `${s.code} · ${s.nameInt}`, "delete");
  };

  return (
    <>
      <PageHead
        title="ติดตามงานติดตั้ง"
        sub="จัดการไซต์ติดตั้ง สถานะ และรูปหน้างาน · เลือกเผยแพร่บางไซต์ขึ้นหน้าเว็บสาธารณะให้ลูกค้าดูได้"
        right={<>
          <Btn icon={I.upload} kind="ghost" onClick={doPublish} disabled={publishing}>
            {publishing ? "กำลังเผยแพร่…" : `เผยแพร่ขึ้นเว็บ (${pubCount})`}
          </Btn>
          <Btn icon={I.plus} kind="primary" onClick={() => setEdit({})}>เพิ่มไซต์</Btn>
        </>}
      />

      <div className="grid cols-4 mb-lg">
        <Stat icon={I.building} label="ไซต์ทั้งหมด" value={`${sites.length} ไซต์`} sub={`${provinces.length} จังหวัด`}/>
        <Stat icon={I.check} label="ติดตั้งแล้ว" value={`${installed} ไซต์`} sub={sites.length ? `${Math.round(installed / sites.length * 100)}% ของทั้งหมด` : "—"}/>
        <Stat icon={I.truck} label="กำลังดำเนินการ" value={`${cnt("scheduled") + cnt("en_route") + cnt("on_site") + cnt("installing")} ไซต์`} sub={`นัดแล้ว ${cnt("scheduled")} · หน้างาน ${cnt("on_site") + cnt("installing")}`}/>
        <Stat icon={I.sparkle} label="เผยแพร่บนเว็บ" value={`${pubCount} ไซต์`} sub={cnt("issue") ? `⚠️ มีปัญหา ${cnt("issue")} ไซต์ (ไม่ขึ้นเว็บ)` : "ลูกค้าดูได้ที่ /track"}/>
      </div>

      <Card flush title={`รายการไซต์ (${shown.length}/${sites.length})`} sub="กดที่แถวเพื่อดูรายละเอียด เปลี่ยนสถานะ และจัดการรูป"
        right={<div style={{display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap"}}>
          <input className="input" style={{height: 30, width: 180, fontSize: 12.5}} placeholder="ค้นหา รหัส/ชื่อ/ลูกค้า" value={q} onChange={e => setQ(e.target.value)}/>
          <select className="input" style={{height: 30, width: "auto", fontSize: 12.5}} value={fStatus} onChange={e => setFStatus(e.target.value)}>
            <option value="">ทุกสถานะ</option>
            {TRK_STATUS_ORDER.map(k => <option key={k} value={k}>{TRK_STATUS[k].label}</option>)}
          </select>
          <select className="input" style={{height: 30, width: "auto", fontSize: 12.5}} value={fProv} onChange={e => setFProv(e.target.value)}>
            <option value="">ทุกจังหวัด</option>
            {provinces.map(p => <option key={p} value={p}>{p}</option>)}
          </select>
          <label style={{display: "flex", alignItems: "center", gap: 5, fontSize: 12}}>
            <input type="checkbox" checked={onlyPub} onChange={e => setOnlyPub(e.target.checked)}/> เฉพาะที่เผยแพร่
          </label>
        </div>}>
        <div className="table-wrap">
          <table className="t">
            <thead><tr>
              <th style={{width: 110}}>รหัส</th><th>ชื่อไซต์ (ภายใน)</th><th style={{width: 150}}>พื้นที่</th>
              <th style={{width: 130}}>สถานะ</th><th style={{width: 110}}>ติดตั้งเสร็จ</th>
              <th style={{width: 90}}>รูป</th><th style={{width: 90}}>เว็บ</th><th style={{width: 70}}></th>
            </tr></thead>
            <tbody>
              {shown.map(s => {
                const st = TRK_STATUS[s.status] || TRK_STATUS.not_installed;
                const ph = photos[s.id] || [];
                const ok = ph.filter(p => p.approved).length;
                return (
                  <tr key={s.id} style={{cursor: "pointer"}} onClick={() => setDetail(s)}>
                    <td className="mono" style={{fontSize: 12}}>{s.code}</td>
                    <td>
                      <div style={{fontSize: 12.5, fontWeight: 600}}>{s.nameInt}</div>
                      {s.customer && <div style={{fontSize: 11, color: "var(--ink-3)"}}>{s.customer}</div>}
                    </td>
                    <td style={{fontSize: 12}}>{[s.district, s.province].filter(Boolean).join(", ") || "—"}</td>
                    <td><span className="badge" style={{background: st.bg, color: st.color, border: "none"}}>{st.emoji} {st.label}</span></td>
                    <td style={{fontSize: 12}}>{s.status === "installed" ? trkThai(s.installedAt) : "—"}</td>
                    <td style={{fontSize: 12}}>{ph.length}{ok ? <span style={{color: "var(--success)"}}> · เผยแพร่ {ok}</span> : ""}</td>
                    <td>{s.isPublic ? <Badge tone="success">เผยแพร่</Badge> : <span style={{fontSize: 11.5, color: "var(--ink-4)"}}>ภายใน</span>}</td>
                    <td onClick={e => e.stopPropagation()}>
                      <div style={{display: "flex", gap: 3, justifyContent: "flex-end"}}>
                        <button className="icon-btn" title="แก้ไข" onClick={() => setEdit(s)}><I.edit size={13}/></button>
                        <button className="icon-btn" title="ลบ" onClick={() => remove(s)}><I.trash size={13}/></button>
                      </div>
                    </td>
                  </tr>
                );
              })}
              {shown.length === 0 && (
                <tr><td colSpan={8} style={{textAlign: "center", padding: 26, color: "var(--ink-3)", fontSize: 12.5}}>
                  {sites.length === 0 ? 'ยังไม่มีไซต์ — กด "เพิ่มไซต์" เพื่อเริ่ม' : "ไม่พบไซต์ที่ตรงกับตัวกรอง"}
                </td></tr>
              )}
            </tbody>
          </table>
        </div>
      </Card>

      <div style={{marginTop: 14, padding: "11px 14px", background: "var(--surface-2)", borderRadius: 10, fontSize: 12, color: "var(--ink-3)", lineHeight: 1.8}}>
        <b style={{color: "var(--ink-1)"}}>ข้อมูลอะไรออกสู่สาธารณะบ้าง:</b> ชื่อที่ตั้งไว้สำหรับเว็บ · จังหวัด/อำเภอ · สถานะแบบ 2 ค่า (ติดตั้งแล้ว/ยังไม่ติดตั้ง) ·
        วันที่ติดตั้งเสร็จ · ข้อความสาธารณะ · พิกัดที่ปัดตามระดับที่เลือก · เฉพาะรูปที่กดอนุมัติ<br/>
        <b style={{color: "var(--ink-1)"}}>สิ่งที่ไม่มีวันหลุด:</b> ชื่อภายใน · ชื่อลูกค้า · ที่อยู่ · ตำบล · พิกัดจริง · วันนัด · ผู้รับผิดชอบ · รายละเอียดภายใน · สถานะ "เกิดปัญหา"
        {" "}(ไซต์ที่ยกเลิกจะไม่ถูกส่งออกเลย)
      </div>

      {edit && <TrkSiteModal site={edit} onClose={() => setEdit(null)} onSaved={() => force(n => n + 1)}/>}
      {detail && <TrkDetailDrawer site={sites.find(s => s.id === detail.id) || detail} onClose={() => setDetail(null)}
        onEdit={(s) => setEdit(s)} onChanged={() => force(n => n + 1)}/>}
    </>
  );
}

window.SiteTrackerPage = SiteTrackerPage;
window.TrackerStore = { get: trkGet, set: trkSet, SITES: TRK_SITES, PHOTOS: TRK_PHOTOS, buildPublic: trkBuildPublic, publish: trkPublish };
